# -*- coding: us-ascii -*-
# frozen_string_literal: false
require 'test/unit'
require 'erb'
require 'stringio'

class TestERB < Test::Unit::TestCase
  class MyError < RuntimeError ; end

  def test_without_filename
    erb = ERB.new("<% raise ::TestERB::MyError %>")
    e = assert_raise(MyError) {
      erb.result
    }
    assert_match(/\A\(erb\):1\b/, e.backtrace[0])
  end

  def test_with_filename
    erb = ERB.new("<% raise ::TestERB::MyError %>")
    erb.filename = "test filename"
    e = assert_raise(MyError) {
      erb.result
    }
    assert_match(/\Atest filename:1\b/, e.backtrace[0])
  end

  def test_with_filename_lineno
    erb = ERB.new("<% raise ::TestERB::MyError %>")
    erb.filename = "test filename"
    erb.lineno = 100
    e = assert_raise(MyError) {
      erb.result
    }
    assert_match(/\Atest filename:101\b/, e.backtrace[0])
  end

  def test_with_location
    erb = ERB.new("<% raise ::TestERB::MyError %>")
    erb.location = ["test filename", 200]
    e = assert_raise(MyError) {
      erb.result
    }
    assert_match(/\Atest filename:201\b/, e.backtrace[0])
  end

  def test_html_escape
    assert_equal(" !&quot;\#$%&amp;&#39;()*+,-./0123456789:;&lt;=&gt;?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~",
                 ERB::Util.html_escape(" !\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"))

    assert_equal("", ERB::Util.html_escape(""))
    assert_equal("abc", ERB::Util.html_escape("abc"))
    assert_equal("&lt;&lt;", ERB::Util.html_escape("<\<"))
    assert_equal("&#39;&amp;&quot;&gt;&lt;", ERB::Util.html_escape("'&\"><"))

    assert_equal("", ERB::Util.html_escape(nil))
    assert_equal("123", ERB::Util.html_escape(123))

    assert_equal(65536+5, ERB::Util.html_escape("x"*65536 + "&").size)
    assert_equal(65536+5, ERB::Util.html_escape("&" + "x"*65536).size)
  end

  def test_html_escape_to_s
    object = Object.new
    def object.to_s
      "object"
    end
    assert_equal("object", ERB::Util.html_escape(object))
  end

  def test_html_escape_extension
    assert_nil(ERB::Util.method(:html_escape).source_location)
  end if RUBY_ENGINE == 'ruby'

  def test_concurrent_default_binding
    # This test randomly fails with JRuby -- NameError: undefined local variable or method `template2'
    pend if RUBY_ENGINE == 'jruby'

    template1 = 'one <%= ERB.new(template2).result %>'

    eval 'template2 = "two"', TOPLEVEL_BINDING

    bug7046 = '[ruby-core:47638]'
    assert_equal("one two", ERB.new(template1).result, bug7046)
  end
end

class TestERBCore < Test::Unit::TestCase
  def setup
    @erb = ERB
  end

  def test_version
    assert_equal(String, @erb.version.class)
  end

  def test_core
    erb = @erb.new("hello")
    assert_equal("hello", erb.result)

    erb = @erb.new("hello", trim_mode: 0)
    assert_equal("hello", erb.result)

    erb = @erb.new("hello", trim_mode: 1)
    assert_equal("hello", erb.result)

    erb = @erb.new("hello", trim_mode: 2)
    assert_equal("hello", erb.result)

    src = <<EOS
%% hi
= hello
<% 3.times do |n| %>
% n=0
* <%= n %>
<% end %>
EOS

    ans = <<EOS
%% hi
= hello

% n=0
* 0

% n=0
* 1

% n=0
* 2

EOS
    erb = @erb.new(src)
    assert_equal(ans, erb.result)
    erb = @erb.new(src, trim_mode: 0)
    assert_equal(ans, erb.result)
    erb = EnvUtil.suppress_warning { @erb.new(src, trim_mode: '') }
    assert_equal(ans, erb.result)

    ans = <<EOS
%% hi
= hello
% n=0
* 0% n=0
* 1% n=0
* 2
EOS
    erb = @erb.new(src, trim_mode: 1)
    assert_equal(ans.chomp, erb.result)
    erb = @erb.new(src, trim_mode: '>')
    assert_equal(ans.chomp, erb.result)

    ans  = <<EOS
%% hi
= hello
% n=0
* 0
% n=0
* 1
% n=0
* 2
EOS

    erb = @erb.new(src, trim_mode: 2)
    assert_equal(ans, erb.result)
    erb = @erb.new(src, trim_mode: '<>')
    assert_equal(ans, erb.result)

    ans = <<EOS
% hi
= hello

* 0

* 0

* 0

EOS
    erb = @erb.new(src, trim_mode: '%')
    assert_equal(ans, erb.result)

    ans = <<EOS
% hi
= hello
* 0* 0* 0
EOS
    erb = @erb.new(src, trim_mode: '%>')
    assert_equal(ans.chomp, erb.result)

    ans = <<EOS
% hi
= hello
* 0
* 0
* 0
EOS
    erb = @erb.new(src, trim_mode: '%<>')
    assert_equal(ans, erb.result)
  end

  def test_trim_line1_with_carriage_return
    erb = @erb.new("<% 3.times do %>\r\nline\r\n<% end %>\r\n", trim_mode: '>')
    assert_equal("line\r\n" * 3, erb.result)

    erb = @erb.new("<% 3.times do %>\r\nline\r\n<% end %>\r\n", trim_mode: '%>')
    assert_equal("line\r\n" * 3, erb.result)
  end

  def test_trim_line2_with_carriage_return
    erb = @erb.new("<% 3.times do %>\r\nline\r\n<% end %>\r\n", trim_mode: '<>')
    assert_equal("line\r\n" * 3, erb.result)

    erb = @erb.new("<% 3.times do %>\r\nline\r\n<% end %>\r\n", trim_mode: '%<>')
    assert_equal("line\r\n" * 3, erb.result)
  end

  def test_explicit_trim_line_with_carriage_return
    erb = @erb.new("<%- 3.times do -%>\r\nline\r\n<%- end -%>\r\n", trim_mode: '-')
    assert_equal("line\r\n" * 3, erb.result)

    erb = @erb.new("<%- 3.times do -%>\r\nline\r\n<%- end -%>\r\n", trim_mode: '%-')
    assert_equal("line\r\n" * 3, erb.result)
  end

  def test_invalid_trim_mode
    pend if RUBY_ENGINE == 'truffleruby'

    assert_warning(/#{__FILE__}:#{__LINE__ + 1}/) do
      @erb.new("", trim_mode: 'abc-def')
    end

    assert_warning(/Invalid ERB trim mode/) do
      @erb.new("", trim_mode: 'abc-def')
    end

    assert_warning(/Invalid ERB trim mode/) do
      @erb.new("", trim_mode: '%<')
    end

    assert_warning(/Invalid ERB trim mode/) do
      @erb.new("", trim_mode: '%<>-')
    end

    assert_warning(/Invalid ERB trim mode/) do
      @erb.new("", trim_mode: 3)
    end
  end

  def test_run
    out = StringIO.new
    orig, $stdout = $stdout, out

    num = 3
    @erb.new('<%= num * 3 %>').run(binding)

    $stdout = orig
    out.rewind
    assert_equal('9', out.read)
    return unless num               # to remove warning
  end

  class Foo; end

  def test_def_class
    erb = @erb.new('hello')
    cls = erb.def_class
    assert_equal(Object, cls.superclass)
    assert_respond_to(cls.new, 'result')
    cls = erb.def_class(Foo)
    assert_equal(Foo, cls.superclass)
    assert_respond_to(cls.new, 'result')
    cls = erb.def_class(Object, 'erb')
    assert_equal(Object, cls.superclass)
    assert_respond_to(cls.new, 'erb')
  end

  def test_percent
    src = <<EOS
%n = 1
<%= n%>
EOS
    assert_equal("1\n", ERB.new(src, trim_mode: '%').result(binding))

    src = <<EOS
<%
%>
EOS
    ans = "\n"
    assert_equal(ans, ERB.new(src, trim_mode: '%').result(binding))

    src = "<%\n%>"
    # ans = "\n"
    ans = ""
    assert_equal(ans, ERB.new(src, trim_mode: '%').result(binding))

    src = <<EOS
<%
n = 1
%><%= n%>
EOS
    assert_equal("1\n", ERB.new(src, trim_mode: '%').result(binding))

    src = <<EOS
%n = 1
%% <% n = 2
n.times do |i|%>
%% %%><%%<%= i%><%
end%>
%%%
EOS
    ans = <<EOS
%\s
% %%><%0
% %%><%1
%%
EOS
    assert_equal(ans, ERB.new(src, trim_mode: '%').result(binding))
  end

  def test_def_erb_method
    klass = Class.new
    klass.module_eval do
      extend ERB::DefMethod
      fname = File.join(File.dirname(File.expand_path(__FILE__)), 'hello.erb')
      def_erb_method('hello', fname)
    end
    assert_respond_to(klass.new, 'hello')

    assert_not_respond_to(klass.new, 'hello_world')
    erb = @erb.new('hello, world')
    klass.module_eval do
      def_erb_method('hello_world', erb)
    end
    assert_respond_to(klass.new, 'hello_world')
  end

  def test_def_method_without_filename
    klass = Class.new
    erb = ERB.new("<% raise ::TestERB::MyError %>")
    erb.filename = "test filename"
    assert_not_respond_to(klass.new, 'my_error')
    erb.def_method(klass, 'my_error')
    e = assert_raise(::TestERB::MyError) {
       klass.new.my_error
    }
    assert_match(/\A\(ERB\):1\b/, e.backtrace[0])
  end

  def test_def_method_with_fname
    klass = Class.new
    erb = ERB.new("<% raise ::TestERB::MyError %>")
    erb.filename = "test filename"
    assert_not_respond_to(klass.new, 'my_error')
    erb.def_method(klass, 'my_error', 'test fname')
    e = assert_raise(::TestERB::MyError) {
       klass.new.my_error
    }
    assert_match(/\Atest fname:1\b/, e.backtrace[0])
  end

  def test_def_module
    klass = Class.new
    klass.include ERB.new('<%= val %>').def_module('render(val)')
    assert_equal('1', klass.new.render(1))
  end

  def test_escape
    src = <<EOS
1.<%% : <%="<%%"%>
2.%%> : <%="%%>"%>
3.
% x = "foo"
<%=x%>
4.
%% print "foo"
5.
%% <%="foo"%>
6.<%="
% print 'foo'
"%>
7.<%="
%% print 'foo'
"%>
EOS
    ans = <<EOS
1.<% : <%%
2.%%> : %>
3.
foo
4.
% print "foo"
5.
% foo
6.
% print 'foo'

7.
%% print 'foo'

EOS
    assert_equal(ans, ERB.new(src, trim_mode: '%').result)
  end

  def test_keep_lineno
    src = <<EOS
Hello,\s
% x = "World"
<%= x%>
% raise("lineno")
EOS

    erb = ERB.new(src, trim_mode: '%')
    e = assert_raise(RuntimeError) {
      erb.result
    }
    assert_match(/\A\(erb\):4\b/, e.backtrace[0].to_s)

    src = <<EOS
%>
Hello,\s
<% x = "World%%>
"%>
<%= x%>
EOS

    ans = <<EOS
%>Hello,\s
World%>
EOS
    assert_equal(ans, ERB.new(src, trim_mode: '>').result)

    ans = <<EOS
%>
Hello,\s

World%>
EOS
    assert_equal(ans, ERB.new(src, trim_mode: '<>').result)

    ans = <<EOS
%>
Hello,\s

World%>

EOS
    assert_equal(ans, ERB.new(src).result)

    src = <<EOS
Hello,\s
<% x = "World%%>
"%>
<%= x%>
<% raise("lineno") %>
EOS

    erb = ERB.new(src)
    e = assert_raise(RuntimeError) {
      erb.result
    }
    assert_match(/\A\(erb\):5\b/, e.backtrace[0].to_s)

    erb = ERB.new(src, trim_mode: '>')
    e = assert_raise(RuntimeError) {
      erb.result
    }
    assert_match(/\A\(erb\):5\b/, e.backtrace[0].to_s)

    erb = ERB.new(src, trim_mode: '<>')
    e = assert_raise(RuntimeError) {
      erb.result
    }
    assert_match(/\A\(erb\):5\b/, e.backtrace[0].to_s)

    src = <<EOS
% y = 'Hello'
<%- x = "World%%>
"-%>
<%= x %><%- x = nil -%>\s
<% raise("lineno") %>
EOS

    erb = ERB.new(src, trim_mode: '-')
    e = assert_raise(RuntimeError) {
      erb.result
    }
    assert_match(/\A\(erb\):5\b/, e.backtrace[0].to_s)

    erb = ERB.new(src, trim_mode: '%-')
    e = assert_raise(RuntimeError) {
      erb.result
    }
    assert_match(/\A\(erb\):5\b/, e.backtrace[0].to_s)
  end

  def test_explicit
    src = <<EOS
<% x = %w(hello world) -%>
NotSkip <%- y = x -%> NotSkip
<% x.each do |w| -%>
  <%- up = w.upcase -%>
  * <%= up %>
<% end -%>
 <%- z = nil -%> NotSkip <%- z = x %>
 <%- z.each do |w| -%>
   <%- down = w.downcase -%>
   * <%= down %>
   <%- up = w.upcase -%>
   * <%= up %>
 <%- end -%>
KeepNewLine <%- z = nil -%>\s
EOS

   ans = <<EOS
NotSkip  NotSkip
  * HELLO
  * WORLD
 NotSkip\s
   * hello
   * HELLO
   * world
   * WORLD
KeepNewLine \s
EOS
   assert_equal(ans, ERB.new(src, trim_mode: '-').result)
   assert_equal(ans, ERB.new(src, trim_mode: '-%').result)
  end

  def test_url_encode
    assert_equal("Programming%20Ruby%3A%20%20The%20Pragmatic%20Programmer%27s%20Guide",
                 ERB::Util.url_encode("Programming Ruby:  The Pragmatic Programmer's Guide"))

    assert_equal("%A5%B5%A5%F3%A5%D7%A5%EB",
                 ERB::Util.url_encode("\xA5\xB5\xA5\xF3\xA5\xD7\xA5\xEB".force_encoding("EUC-JP")))

    assert_equal("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~",
                 ERB::Util.url_encode("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"),
                 "should not escape any unreserved characters, as per RFC3986 Section 2.3")
  end

  def test_percent_after_etag
    assert_equal("1%", @erb.new("<%= 1 %>%", trim_mode: "%").result)
  end

  def test_token_extension
    extended_erb = Class.new(ERB)
    extended_erb.module_eval do
      def make_compiler(trim_mode)
        compiler = Class.new(ERB::Compiler)
        compiler.module_eval do
          def compile_stag(stag, out, scanner)
            case stag
            when '<%=='
              scanner.stag = stag
              add_put_cmd(out, content) if content.size > 0
              self.content = ''
            else
              super
            end
          end

          def compile_content(stag, out)
            case stag
            when '<%=='
              out.push("#{@insert_cmd}(::ERB::Util.html_escape(#{content}))")
            else
              super
            end
          end

          def make_scanner(src)
            scanner = Class.new(ERB::Compiler::SimpleScanner)
            scanner.module_eval do
              def stags
                ['<%=='] + super
              end
            end
            scanner.new(src, @trim_mode, @percent)
          end
        end
        compiler.new(trim_mode)
      end
    end

    src = <<~EOS
      <% tag = '<>' \%>
      <\%= tag \%>
      <\%== tag \%>
    EOS
    ans = <<~EOS

      <>
      &lt;&gt;
    EOS
    assert_equal(ans, extended_erb.new(src).result)
  end

  def test_frozen_string_literal
    bug12031 = '[ruby-core:73561] [Bug #12031]'
    e = @erb.new("<%#encoding: us-ascii%>a")
    e.src.sub!(/\A#(?:-\*-)?(.*)(?:-\*-)?/) {
      '# -*- \1; frozen-string-literal: true -*-'
    }
    assert_equal("a", e.result, bug12031)

    %w(false true).each do |flag|
      erb = @erb.new("<%#frozen-string-literal: #{flag}%><%=''.frozen?%>")
      assert_equal(flag, erb.result)
    end
  end

  def test_result_with_hash
    erb = @erb.new("<%= foo %>")
    assert_equal("1", erb.result_with_hash(foo: "1"))
  end

  def test_result_with_hash_does_not_use_caller_local_variables
    erb = @erb.new("<%= foo %>")
    foo = 1
    assert_raise(NameError) { erb.result_with_hash({}) }
    assert_equal("1", erb.result_with_hash(foo: foo))
  end

  def test_result_with_hash_does_not_modify_caller_binding
    erb = @erb.new("<%= foo %>")
    erb.result_with_hash(foo: "1")
    assert_equal(false, binding.local_variable_defined?(:foo))
  end

  def test_result_with_hash_does_not_modify_toplevel_binding
    erb = @erb.new("<%= foo %>")
    erb.result_with_hash(foo: "1")
    assert_equal(false, TOPLEVEL_BINDING.local_variable_defined?(:foo))
    TOPLEVEL_BINDING.eval 'template2 = "two"'
    erb = @erb.new("<%= template2 %>")
    erb.result_with_hash(template2: "TWO")
    assert_equal "two", TOPLEVEL_BINDING.local_variable_get("template2")
  end

  # This depends on the behavior that #local_variable_set raises TypeError by invalid key.
  def test_result_with_hash_with_invalid_keys_raises_type_error
    erb = @erb.new("<%= 1 %>")
    assert_raise(TypeError) { erb.result_with_hash({ 1 => "1" }) }
  end

  # Bug#14243
  def test_half_working_comment_backward_compatibility
    assert_nothing_raised do
      @erb.new("<% # comment %>\n").result
    end
  end

  def test_prohibited_marshal_dump
    erb = ERB.new("")
    assert_raise(TypeError) {Marshal.dump(erb)}
  end

  def test_prohibited_marshal_load
    erb = ERB.allocate
    erb.instance_variable_set(:@src, "")
    erb.instance_variable_set(:@lineno, 1)
    erb.instance_variable_set(:@_init, true)
    erb = Marshal.load(Marshal.dump(erb))
    assert_raise(ArgumentError) {erb.result}
  end

  def test_multi_line_comment_lineno
    erb = ERB.new(<<~EOS)
      <%= __LINE__ %>
      <%#
      %><%= __LINE__ %>
    EOS
    assert_equal <<~EOS, erb.result
      1
      3
    EOS
  end
end

class TestERBCoreWOStrScan < TestERBCore
  def setup
    @save_map = ERB::Compiler::Scanner.instance_variable_get('@scanner_map')
    map = {[nil, false]=>ERB::Compiler::SimpleScanner}
    ERB::Compiler::Scanner.instance_variable_set('@scanner_map', map)
    super
  end

  def teardown
    ERB::Compiler::Scanner.instance_variable_set('@scanner_map', @save_map)
  end
end
