# -*- coding: utf-8 -*-
require "minitest/autorun"
require "execjs/module"
require "json"

begin
  require "execjs"
  ExecJS.runtime
rescue ExecJS::RuntimeUnavailable => e
  warn e
  exit 2
end

if defined? Minitest::Test
  Test = Minitest::Test
elsif defined? MiniTest::Unit::TestCase
  Test = MiniTest::Unit::TestCase
end

class TestExecJS < Test
  def test_runtime_available
    runtime = ExecJS::ExternalRuntime.new(command: "nonexistent")
    assert !runtime.available?

    runtime = ExecJS::ExternalRuntime.new(command: "ruby")
    assert runtime.available?
  end

  def test_runtime_assignment
    original_runtime = ExecJS.runtime
    runtime = ExecJS::ExternalRuntime.new(command: "nonexistent")
    assert_raises(ExecJS::RuntimeUnavailable) { ExecJS.runtime = runtime }
    assert_equal original_runtime, ExecJS.runtime

    runtime = ExecJS::ExternalRuntime.new(command: "ruby")
    ExecJS.runtime = runtime
    assert_equal runtime, ExecJS.runtime
  ensure
    ExecJS.runtime = original_runtime
  end

  def test_context_call
    context = ExecJS.compile("id = function(v) { return v; }")
    assert_equal "bar", context.call("id", "bar")
  end

  def test_nested_context_call
    context = ExecJS.compile("a = {}; a.b = {}; a.b.id = function(v) { return v; }")
    assert_equal "bar", context.call("a.b.id", "bar")
  end

  def test_call_with_complex_properties
    context = ExecJS.compile("")
    assert_equal 2, context.call("function(a, b) { return a + b }", 1, 1)

    context = ExecJS.compile("foo = 1")
    assert_equal 2, context.call("(function(bar) { return foo + bar })", 1)
  end

  def test_call_with_env_file
    Dir.mktmpdir do |dir|
      Dir.chdir(dir) do
        # Bun prints on STDOUT when loading .env files
        File.write(".env", "FOO=BAR")
        assert_equal 2, ExecJS.eval("1 + 1")
      end
    end
  end

  def test_call_with_this
    # Known bug: https://github.com/cowboyd/therubyrhino/issues/39
    skip if ExecJS.runtime.is_a?(ExecJS::RubyRhinoRuntime)

    # Make sure that `this` is indeed the global scope
    context = ExecJS.compile(<<-EOF)
      name = 123;

      function Person(name) {
        this.name = name;
      }

      Person.prototype.getThis = function() {
        return this.name;
      }
    EOF

    assert_equal 123, context.call("(new Person('Bob')).getThis")
  end

  def test_context_call_missing_function
    context = ExecJS.compile("")
    assert_raises ExecJS::ProgramError do
      context.call("missing")
    end
  end

  {
    "function() {}" => nil,
    "0" => 0,
    "null" => nil,
    "undefined" => nil,
    "true" => true,
    "false" => false,
    "[1, 2]" => [1, 2],
    "[1, function() {}]" => [1, nil],
    "'hello'" => "hello",
    "'red yellow blue'.split(' ')" => ["red", "yellow", "blue"],
    "{a:1,b:2}" => {"a"=>1,"b"=>2},
    "{a:true,b:function (){}}" => {"a"=>true},
    "'café'" => "café",
    '"☃"' => "☃",
    '"\u2603"' => "☃",
    "'\u{1f604}'".encode("UTF-8") => "\u{1f604}".encode("UTF-8"), # Smiling emoji
    "'\u{1f1fa}\u{1f1f8}'".encode("UTF-8") => "\u{1f1fa}\u{1f1f8}".encode("UTF-8"), # US flag
    '"\\\\"' => "\\"
  }.each_with_index do |(input, output), index|
    define_method("test_exec_string_#{index}") do
      assert_output output, ExecJS.exec("return #{input}")
    end

    define_method("test_eval_string_#{index}") do
      assert_output output, ExecJS.eval(input)
    end

    define_method("test_compile_return_string_#{index}") do
      context = ExecJS.compile("var a = #{input};")
      assert_output output, context.eval("a")
    end

    define_method("test_compile_call_string_#{index}") do
      context = ExecJS.compile("function a() { return #{input}; }")
      assert_output output, context.call("a")
    end
  end

  [
    nil,
    true,
    false,
    1,
    3.14,
    "hello",
    "\\",
    "café",
    "☃",
    "\u{1f604}".encode("UTF-8"), # Smiling emoji
    "\u{1f1fa}\u{1f1f8}".encode("UTF-8"), # US flag
    [1, 2, 3],
    [1, [2, 3]],
    [1, [2, [3]]],
    ["red", "yellow", "blue"],
    { "a" => 1, "b" => 2},
    { "a" => 1, "b" => [2, 3]},
    { "a" => true }
  ].each_with_index do |value, index|
    json_value = JSON.generate(value, quirks_mode: true)

    define_method("test_json_value_#{index}") do
      assert_output value, JSON.parse(json_value, quirks_mode: true)
    end

    define_method("test_exec_value_#{index}") do
      assert_output value, ExecJS.exec("return #{json_value}")
    end

    define_method("test_eval_value_#{index}") do
      assert_output value, ExecJS.eval("#{json_value}")
    end

    define_method("test_stringify_value_#{index}") do
      context = ExecJS.compile("function json(obj) { return JSON.stringify(obj); }")
      assert_output json_value, context.call("json", value)
    end

    define_method("test_call_value_#{index}") do
      context = ExecJS.compile("function id(obj) { return obj; }")
      assert_output value, context.call("id", value)
    end
  end

  def test_symbol
    context = ExecJS.compile("function echo(test) { return test; }")
    assert_equal "symbol", context.call("echo", :symbol)
    assert_equal ["symbol"], context.call("echo", [:symbol])
    assert_equal({"key" => "value"}, context.call("echo", {key: :value}))

    context = ExecJS.compile("function myslice(str) { return str.slice(1); }")
    assert_equal "ymbol", context.call("myslice", :symbol)
  end

  def test_additional_options
    assert ExecJS.eval("true", :foo => true)
    assert ExecJS.exec("return true", :foo => true)

    context = ExecJS.compile("foo = true", :foo => true)
    assert context.eval("foo", :foo => true)
    assert context.exec("return foo", :foo => true)
  end

  def test_eval_blank
    assert_nil ExecJS.eval("")
    assert_nil ExecJS.eval(" ")
    assert_nil ExecJS.eval("  ")
  end

  def test_exec_return
    assert_nil ExecJS.exec("return")
  end

  def test_exec_no_return
    assert_nil ExecJS.exec("1")
  end

  def test_encoding
    utf8 = Encoding.find('UTF-8')

    assert_equal utf8, ExecJS.exec("return 'hello'").encoding
    assert_equal utf8, ExecJS.eval("'☃'").encoding

    ascii = "'hello'".encode('US-ASCII')
    result = ExecJS.eval(ascii)
    assert_equal "hello", result
    assert_equal utf8, result.encoding

    assert_raises Encoding::UndefinedConversionError do
      binary = "\xde\xad\xbe\xef".force_encoding("BINARY")
      ExecJS.eval(binary)
    end
  end

  def test_encoding_compile
    utf8 = Encoding.find('UTF-8')

    context = ExecJS.compile("foo = function(v) { return '¶' + v; }".encode("ISO8859-15"))

    assert_equal utf8, context.exec("return foo('hello')").encoding
    assert_equal utf8, context.eval("foo('☃')").encoding

    ascii = "foo('hello')".encode('US-ASCII')
    result = context.eval(ascii)
    assert_equal "¶hello", result
    assert_equal utf8, result.encoding

    assert_raises Encoding::UndefinedConversionError do
      binary = "\xde\xad\xbe\xef".force_encoding("BINARY")
      context.eval(binary)
    end
  end

  def test_surrogate_pairs
    # Smiling emoji
    str = "\u{1f604}".encode("UTF-8")
    assert_equal 2, ExecJS.eval("'#{str}'.length")
    assert_equal str, ExecJS.eval("'#{str}'")

    # US flag emoji
    str = "\u{1f1fa}\u{1f1f8}".encode("UTF-8")
    assert_equal 4, ExecJS.eval("'#{str}'.length")
    assert_equal str, ExecJS.eval("'#{str}'")
  end

  def test_compile_anonymous_function
    context = ExecJS.compile("foo = function() { return \"bar\"; }")
    assert_equal "bar", context.exec("return foo()")
    assert_equal "bar", context.eval("foo()")
    assert_equal "bar", context.call("foo")
  end

  def test_compile_named_function
    context = ExecJS.compile("function foo() { return \"bar\"; }")
    assert_equal "bar", context.exec("return foo()")
    assert_equal "bar", context.eval("foo()")
    assert_equal "bar", context.call("foo")
  end

  def test_this_is_global_scope
    assert_equal true, ExecJS.eval("this === (function() {return this})()")
    assert_equal true, ExecJS.exec("return this === (function() {return this})()")
  end

  def test_browser_self_is_undefined
    assert ExecJS.eval("typeof self == 'undefined'")
  end

  def test_node_global_is_undefined
    assert ExecJS.eval("typeof global == 'undefined'")
  end

  def test_node_process_is_undefined
    assert ExecJS.eval("typeof process == 'undefined'")
    refute ExecJS.eval("'process' in this")
  end

  def test_commonjs_vars_are_undefined
    assert ExecJS.eval("typeof module == 'undefined'")
    assert ExecJS.eval("typeof exports == 'undefined'")
    assert ExecJS.eval("typeof require == 'undefined'")

    refute ExecJS.eval("'module' in this")
    refute ExecJS.eval("'exports' in this")
    refute ExecJS.eval("'require' in this")
  end

  def test_console_is_undefined
    assert ExecJS.eval("typeof console == 'undefined'")
    refute ExecJS.eval("'console' in this")
  end

  def test_timers_are_undefined
    # See Bug https://github.com/oven-sh/bun/issues/4806
    skip if ENV["EXECJS_RUNTIME"] == "Bun"

    assert ExecJS.eval("typeof setTimeout == 'undefined'")
    assert ExecJS.eval("typeof setInterval == 'undefined'")
    assert ExecJS.eval("typeof clearTimeout == 'undefined'")
    assert ExecJS.eval("typeof clearInterval == 'undefined'")
    assert ExecJS.eval("typeof setImmediate == 'undefined'")
    assert ExecJS.eval("typeof clearImmediate == 'undefined'")

    refute ExecJS.eval("'setTimeout' in this")
    refute ExecJS.eval("'setInterval' in this")
    refute ExecJS.eval("'clearTimeout' in this")
    refute ExecJS.eval("'clearInterval' in this")
    refute ExecJS.eval("'setImmediate' in this")
    refute ExecJS.eval("'clearImmediate' in this")
  end

  def test_compile_large_scripts
    body = "var foo = 'bar';\n" * 100_000
    assert ExecJS.exec("function foo() {\n#{body}\n};\nreturn true")
  end

  def test_large_return_value
    string = ExecJS.eval('(new Array(100001)).join("abcdef")')
    assert_equal 600_000, string.size
  end

  def test_exec_syntax_error
    begin
      ExecJS.exec(")")
      flunk
    rescue ExecJS::RuntimeError => e
      assert e
      assert e.backtrace.join("\n").include?("(execjs):")
    end
  end

  def test_eval_syntax_error
    begin
      ExecJS.eval(")")
      flunk
    rescue ExecJS::RuntimeError => e
      assert e
      assert e.backtrace.join("\n").include?("(execjs):")
    end
  end

  def test_compile_syntax_error
    begin
      ExecJS.compile(")")
      flunk
    rescue ExecJS::RuntimeError => e
      assert e
      assert e.backtrace[0].include?("(execjs):"), e.backtrace.join("\n")
    end
  end

  def test_exec_thrown_error
    begin
      ExecJS.exec("throw new Error('hello')")
      flunk
    rescue ExecJS::ProgramError => e
      assert e
      assert e.backtrace.join("\n").include?("(execjs):")
    end
  end

  def test_eval_thrown_error
    begin
      ExecJS.eval("(function(){ throw new Error('hello') })()")
      flunk
    rescue ExecJS::ProgramError => e
      assert e
      assert e.backtrace.join("\n").include?("(execjs):")
    end
  end

  def test_compile_thrown_error
    begin
      ExecJS.compile("throw new Error('hello')")
      flunk
    rescue ExecJS::ProgramError => e
      assert e
      assert e.backtrace.join("\n").include?("(execjs):")
    end
  end

  def test_exec_thrown_string
    assert_raises ExecJS::ProgramError do
      ExecJS.exec("throw 'hello'")
    end
  end

  def test_eval_thrown_string
    assert_raises ExecJS::ProgramError do
      ExecJS.eval("(function(){ throw 'hello' })()")
    end
  end

  def test_compile_thrown_string
    assert_raises ExecJS::ProgramError do
      ExecJS.compile("throw 'hello'")
    end
  end

  def test_babel
    skip if ExecJS.runtime.is_a?(ExecJS::RubyRhinoRuntime)

    assert source = File.read(File.expand_path("../fixtures/babel.js", __FILE__))
    source = <<-JS
      var self = this;
      #{source}
      babel.eval = function(code) {
        return eval(babel.transform(code)["code"]);
      }
    JS
    context = ExecJS.compile(source)
    assert_equal 64, context.call("babel.eval", "((x) => x * x)(8)")
  end

  def test_coffeescript
    assert source = File.read(File.expand_path("../fixtures/coffee-script.js", __FILE__))
    context = ExecJS.compile(source)
    assert_equal 64, context.call("CoffeeScript.eval", "((x) -> x * x)(8)")
  end

  def test_uglify
    assert source = File.read(File.expand_path("../fixtures/uglify.js", __FILE__))
    source = <<-JS
      #{source}

      function uglify(source) {
        var ast = UglifyJS.parse(source);
        var stream = UglifyJS.OutputStream();
        ast.print(stream);
        return stream.toString();
      }
    JS
    context = ExecJS.compile(source)
    assert_equal "function foo(bar){return bar}",
      context.call("uglify", "function foo(bar) {\n  return bar;\n}")
  end

  def test_async_bun
    skip unless ENV["EXECJS_RUNTIME"] == "Bun"
    source = <<-JS
      async function testAsync() { return (await new Promise((resolve) => { resolve("it works!") } )) }
    JS
    context = ExecJS.compile(source)
    assert_equal "it works!", context.call("testAsync")
  end

  private

    def assert_output(expected, actual)
      if expected.nil?
        assert_nil actual
      else
        assert_equal expected, actual
      end
    end
end
