#
# Copyright (C) Dmitry Volyntsev
# Copyright (C) NGINX, Inc.
#

proc njs_test {body {opts ""}} {

    if {$opts eq ""} {
        spawn  -nottycopy njs

    } else {
        eval spawn  -nottycopy njs $opts
    }

    expect -re "interactive njs \\((njs|QuickJS):.*\r\n\r\n>> "

    set len [llength $body]
    for {set i 0} {$i < $len} {incr i} {
        set pair [lindex $body $i]
        send [lindex $pair 0]
        expect [lindex $pair 1]
    }

    send "\n"
    send ".exit\r\n"
    expect eof
}

proc njs_run {opts expected_re} {
    catch {exec njs {*}$opts} out
    if {[regexp $expected_re $out match] == 0} {
        return -code error "njs_run: unexpected output '$out' vs '$expected_re'"
    }
}

njs_test {
    {"njs.version\r\n"
     "njs.version\r\n\*\.\*\.\*"}
}

# simple multi line interaction
njs_test {
    {"var a = 1\r\n"
     "var a = 1\r\nundefined\r\n>> "}
    {"a *= 2\r\n"
     "a *= 2\r\n2\r\n>> "}
}

# function declarations in interactive mode
njs_test {
    {"function a() { return 1; }\r\n"
     "undefined\r\n>> "}
    {"a();\r\n"
     "1\r\n>> "}
    {"function a() { return 2; }\r\n"
     "undefined\r\n>> "}
    {"a();\r\n"
     "2\r\n>> "}
}

# Global completions

njs_test {
    {"\t\t"
     "$262*"}
}

# Global completions, single partial match

njs_test {
    {"O\t"
     "O\a*bject"}
}

njs_test {
    {"Mat\t"
     "Mat\a*h"}
}

 njs_test {
     {"conso\t"
     "conso\a*le"}
 }

# Global completions, multiple partial match
njs_test {
    {"cons\t\t"
     "console*const"}
}

njs_test {
    {"Type\t"
     "Type\a*Error"}
    {".\t\t"
     "TypeError.__proto__"}
}

njs_test {
    {"TypeError.\t\t"
     "TypeError.__proto__*TypeError.prototype"}
}

njs_test {
    {"Object.ge\t"
     "Object.ge\a*t"}
    {"\t\t"
     "Object.getOwnPropertyDescriptor*Object.getPrototypeOf"}
}

njs_test {
    {"JS\t"
     "JS\a*ON"}
    {".\t\t"
     "JSON.__*JSON.stringify"}
}

njs_test {
    {"decodeURI\t\t"
     "decodeURI*decodeURIComponent"}
    {"C\t"
     "decodeURIC\a*omponent"}
}

# Global completions, no matches
njs_test {
    {"1.\t\t"
     "1."}
}

njs_test {
    {"1..\t\t"
     "1.."}
}

njs_test {
    {"'abc'.\t\t"
     "'abc'."}
}

# Global completions, global vars
njs_test {
    {"var AA = 1; var AAA = 2\r\n"
     "var AA = 1; var AAA = 2\r\nundefined\r\n>> "}
    {"AA\t\t"
     "AA*AAA*"}
}

njs_test {
    {"var zz = 1\r\n"
     "var zz = 1\r\nundefined\r\n>> "}
    {"1 + z\t\r\n"
     "1 + z*z*\r\n2"}
}

njs_test {
    {"unknown_var\t\t"
     "unknown_var"}
}

njs_test {
    {"unknown_var.\t\t"
     "unknown_var."}
}

# An object's level completions
njs_test {
    {"var o = {zz:1, zb:2}\r\n"
     "var o = {zz:1, zb:2}\r\nundefined\r\n>> "}
    {"o.z\t\t"
     "o.zb*o.zz"}
}

njs_test {
    {"var d = new Date()\r\n"
     "var d = new Date()\r\nundefined\r\n>> "}
    {"d.to\t\t"
     "d.toDateString*d.toLocaleDateString*d.toString"}
}

njs_test {
    {"var o = {a:new Date()}\r\n"
     "var o = {a:new Date()}\r\nundefined\r\n>> "}
    {"o.a.to\t\t"
     "o.a.toDateString*o.a.toLocaleDateString*o.a.toString"}
}

njs_test {
    {"var o = {a:1,b:2,333:'t'}\r\n"
     "var o = {a:1,b:2,333:'t'}\r\nundefined\r\n>> "}
    {"o.3\t\t"
     "o.3"}
}

njs_test {
    {"var a = Array(5000000); a.aab = 1; a.aac = 2\r\n"
     "var a = Array(5000000); a.aab = 1; a.aac = 2\r\n2\r\n>> "}
    {"a.\t\t"
     "a.aab*"}
}

njs_test {
    {"var a = new Uint8Array([5,6,7,8,8]); a.aab = 1; a.aac = 2\r\n"
     "var a = new Uint8Array(\\\[5,6,7,8,8]); a.aab = 1; a.aac = 2\r\n2\r\n>> "}
    {"a.\t\t"
     "a.aab*"}
}

# console object
njs_test {
    {"console[Symbol.toStringTag]\r\n"
     "console\\\[Symbol.toStringTag]\r\n'Console'\r\n>> "}
    {"Object.prototype.toString.call(console)\r\n"
     "Object.prototype.toString.call(console)\r\n'\\\[object Console]'\r\n>> "}
    {"console.toString()\r\n"
     "console.toString()\r\n'\\\[object Console]'\r\n>> "}
}

# console log functions
njs_test {
    {"console.log.length\r\n"
     "console.log.length\r\n0"}
    {"console.log()\r\n"
     "console.log()\r\nundefined\r\n>> "}
    {"console.log('')\r\n"
     "console.log('')\r\n\r\nundefined\r\n>> "}
    {"console.log(1)\r\n"
     "console.log(1)\r\n1\r\nundefined\r\n>> "}
    {"console.log(1, 'a')\r\n"
     "console.log(1, 'a')\r\n1\r\na\r\nundefined\r\n>> "}
    {"print(1, 'a')\r\n"
     "print(1, 'a')\r\n1\r\na\r\nundefined\r\n>> "}
    {"console.log('\\tабв\\nгд')\r\n"
     "console.log('\\\\tабв\\\\nгд')\r\n\tабв\r\nгд\r\nundefined\r\n>> "}
    {"console.error(42)\r\n"
     "console.error(42)\r\nE: 42\r\nundefined\r\n>> "}
    {"console.info(23)\r\n"
     "console.info(23)\r\n23\r\nundefined\r\n>> "}
    {"console.warn(37)\r\n"
     "console.warn(37)\r\nW: 37\r\nundefined\r\n>> "}
}

# console.time* functions
njs_test {
    {"console.time()\r\n"
     "console.time()\r\nundefined\r\n>> "}
    {"console.timeEnd()\r\n"
     "console.timeEnd()\r\ndefault: *.*ms\r\nundefined\r\n>> "}
    {"console.time(undefined)\r\n"
     "console.time(undefined)\r\nundefined\r\n>> "}
    {"console.timeEnd(undefined)\r\n"
     "console.timeEnd(undefined)\r\ndefault: *.*ms\r\nundefined\r\n>> "}
    {"console.time('abc')\r\n"
     "console.time('abc')\r\nundefined\r\n>> "}
    {"console.time('abc')\r\n"
     "console.time('abc')\r\nTimer \"abc\" already exists.\r\nundefined\r\n>> "}
    {"console.timeEnd('abc')\r\n"
     "console.timeEnd('abc')\r\nabc: *.*ms\r\nundefined\r\n>> "}
    {"console.time(true)\r\n"
     "console.time(true)\r\nundefined\r\n>> "}
    {"console.timeEnd(true)\r\n"
     "console.timeEnd(true)\r\ntrue: *.*ms\r\nundefined\r\n>> "}
    {"console.time(42)\r\n"
     "console.time(42)\r\nundefined\r\n>> "}
    {"console.timeEnd(42)\r\n"
     "console.timeEnd(42)\r\n42: *.*ms\r\nundefined\r\n>> "}
    {"console.timeEnd()\r\n"
     "console.timeEnd()\r\nTimer \"default\" doesn’t exist."}
    {"console.timeEnd('abc')\r\n"
     "console.timeEnd('abc')\r\nTimer \"abc\" doesn’t exist."}
    {"console.time('abc')\r\n"
     "console.time('abc')\r\nundefined\r\n>> "}
}

njs_test {
    {"var print = console.log.bind(console); print(1, 'a', [1, 2])\r\n"
     "1\r\na\r\n*1,2*\r\nundefined\r\n>> "}
    {"var print = console.log.bind(console); print(console.a.a)\r\n"
     "TypeError: cannot * property *a* of undefined*"}
    {"print(console.a.a)\r\n"
     "TypeError: cannot * property *a* of undefined*"}
}

# Backtraces are reset between invocations
njs_test {
    {"JSON.parse(Error())\r\n"
     "JSON.parse(Error())\r\nThrown:\r\nSyntaxError: *nexpected token"}
    {"console.a.a\r\n"
     "TypeError: cannot * property *a* of undefined*"}
}

njs_test {
    {"try { console.log({ toString: function() { throw 'test'; } }) } catch (e) {}\r\n"
     "undefined"}
    {"function f() { throw 't' }; try { console.log({ toString: function() { return f() } }) } catch (e) {}\r\n"
     "undefined"}
}

njs_test {
    {"(function() { throw 'test' })()\r\n"
     "Thrown:\r\n*test"}
}

njs_test {
    {"function f() { return ({}.a.a); }\r\n"
     "undefined\r\n>> "}
    {"var e; try {f()} catch (ee) {e = ee}; undefined\r\n"
     "undefined\r\n>> "}
    {"Object.keys(null)\r\n"
     "Thrown:\r\nTypeError: *annot convert*to object"}
    {"e\r\n"
     "TypeError: cannot * property *a* of undefined"}
}

# Non-ASCII characters
njs_test {
    {"'絵文字'\r\n"
     "'絵文字'"}
    {"var v = 'абвгдеёжзийкл';v[10]\r\n"
     "'й'"}
}

# Immediate events

njs_test {
    {"var t = setImmediate(console.log, 'a', 'aa')\r\n"
     "undefined\r\na\r\naa"}
}

njs_test {
    {"var a = 1 + 1; setTimeout(function (x) {a = x}, 0, 'a'); a\r\n"
     "2"}
    {"a\r\n"
     "a\r\n'a'"}
}

njs_test {
    {"setTimeout(function () {}, 1, 'a')\r\n"
     "InternalError: njs_set_timer(): async timers unsupported"}
}

njs_test {
    {"var a = 1 + 1; setTimeout(function (x) { setTimeout(function (y) {a = y}, 0, x)}, 0, 'a'); a\r\n"
     "2"}
    {"a\r\n"
     "a\r\n'a'"}
}

njs_test {
    {"var a = 1 + 1; setImmediate(function (x) { setImmediate(function (y) {a = y}, x)}, 'a'); a\r\n"
     "2"}
    {"a\r\n"
     "a\r\n'a'"}
}

njs_test {
    {"var i = 0; (function x() { if (i < 10) setImmediate(x); i++; })()\r\n"
     "undefined"}
    {"i\r\n"
     "i\r\n11"}
}

njs_test {
    {"var a = 0, t = setImmediate(function() {a = 1}); clearTimeout(t)\r\n"
     "undefined"}
    {"a\r\n"
     "a\r\n0"}
}

njs_test {
    {"var i = 0; (function x() { if (i < 3) setImmediate(x); i++; throw 'Oops';})()\r\n"
     "Oops"}
    {"i\r\n"
     "i\r\n4"}
}

njs_test {
    {"var i = 0, queue = []; (function x() { if (i < 5) setImmediate(x); queue.push(i++); })()\r\n"
     "undefined"}
    {"queue.toString()\r\n"
     "queue.toString()\r\n'0,1,2,3,4,5'"}
}

njs_run {"-c" "setTimeout(() => {console.log('A'.repeat(1024))}, 0); ref"} \
"^Thrown:
ReferenceError: .* is not defined.*"

njs_run {"-c" "setTimeout(() => {ref}, 0); setTimeout(() => {console.log('A'.repeat(1024))}, 0)"} \
"^Thrown:
ReferenceError: .* is not defined.*"

njs_test {
    {"setImmediate(() => { console.log('x'); return Promise.reject('xx'); })\r\n"
     "0\r\nx\r\nThrown:\r\n*Error: unhandled promise rejection: xx\r\n"}
    {"setImmediate(() => { console.log('x'); return Promise.reject('xx'); })\r\n"
     "1\r\nx\r\nThrown:\r\n*Error: unhandled promise rejection: xx\r\n"}
    {"42\r\n"
     "42\r\n"}
}

# CLI OPTIONS

# help

njs_run {"-h"} "Options"

# command

njs_run {"-c" "console.log(\"a b c\")"} "a b c"

njs_run {"-c" "console.log("} "SyntaxError: \[Uu\]nexpected"


# process

njs_run {"-c" "console.log(typeof process.argv)"} "object"
njs_run {"-c" "console.log(process.argv.slice(2))" "AAA"} "AAA"

njs_run {"-c" "console.log(typeof process.env)"} "object"
njs_run {"-c" "console.log(process.env.HOME != undefined)"} "true"
njs_run {"-c" "console.log(process.env.___UNDECLARED != undefined)"} "false"

njs_run {"-c" "console.log(process.pid)"} "\\d+"

njs_run {"-c" "console.log(process.ppid)"} "\\d+"

njs_run {"-c" "console.log(process.kill(process.pid, 0))"} "true"
njs_run {"-c" "console.log(process.kill(process.pid, 'SIGCHLD'))"} "true"
njs_run {"-c" "process.kill(process.pid, 'SIG\\u0100')"} "TypeError: .*signal"


# script args

njs_run {"test/script_args.js" "A" "B"} "AB"

# version

njs_run {"-v"} "\\d+\.\\d+\.\\d+"
