// Syntax suite: covers every syntactic feature of cell script
// Run: ./cell --mach-run syntax_suite.ce

var passed = 0
var failed = 0
var error_names = []
var error_reasons = []
var fail_msg = ""

var _i = 0
for (_i = 0; _i < 100; _i++) {
  error_names[] = null
  error_reasons[] = null
}

var fail = function(msg) {
  fail_msg = msg
  disrupt
}

var assert_eq = function(actual, expected, msg) {
  if (actual != expected) fail(msg + " (got=" + text(actual) + " expected=" + text(expected) + ")")
}

var run = function(name, fn) {
  fail_msg = ""
  fn()
  passed = passed + 1
} disruption {
  error_names[failed] = name
  error_reasons[failed] = fail_msg == "" ? "disruption" : fail_msg
  failed = failed + 1
}

var should_disrupt = function(fn) {
  var caught = false
  var wrapper = function() {
    fn()
  } disruption {
    caught = true
  }
  wrapper()
  return caught
}

// === LITERALS ===

run("number literals", function() {
  assert_eq(42, 42, "integer")
  assert_eq(3.14 > 3, true, "float")
  assert_eq(-5, -5, "negative")
  assert_eq(0, 0, "zero")
  assert_eq(1e3, 1000, "scientific")
})

run("string literals", function() {
  assert_eq("hello", "hello", "double quote")
  assert_eq("", "", "empty string")
  assert_eq("line1\nline2" != "line1line2", true, "escape sequence")
})

run("template literals", function() {
  var x = "world"
  assert_eq(`hello ${x}`, "hello world", "interpolation")
  assert_eq(`${1 + 2}`, "3", "expression interpolation")
  assert_eq(`plain`, "plain", "no interpolation")
})

run("boolean literals", function() {
  assert_eq(true, true, "true")
  assert_eq(false, false, "false")
})

run("null literal", function() {
  assert_eq(null, null, "null")
})

run("array literal", function() {
  var a = [1, 2, 3]
  assert_eq(length(a), 3, "array length")
  assert_eq(a[0], 1, "first element")
  var e = []
  assert_eq(length(e), 0, "empty array")
})

run("object literal", function() {
  var o = {a: 1, b: "two"}
  assert_eq(o.a, 1, "object prop")
  var e = {}
  assert_eq(e.x, null, "empty object missing prop")
})

run("regex literal", function() {
  var r = /\d+/
  var result = extract("abc123", r)
  assert_eq(result[0], "123", "regex match")
  var ri = /hello/i
  var result2 = extract("Hello", ri)
  assert_eq(result2[0], "Hello", "regex flags")
})

// === DECLARATIONS ===

run("var declaration", function() {
  var x = 5
  assert_eq(x, 5, "var init")
})

run("var multiple declaration", function() {
  var a = 1, b = 2, c = 3
  assert_eq(a + b + c, 6, "multi var")
})

run("def declaration", function() {
  def x = 42
  assert_eq(x, 42, "def const")
})

// === ARITHMETIC OPERATORS ===

run("arithmetic operators", function() {
  assert_eq(2 + 3, 5, "add")
  assert_eq(5 - 3, 2, "sub")
  assert_eq(3 * 4, 12, "mul")
  assert_eq(12 / 4, 3, "div")
  assert_eq(10 % 3, 1, "mod")
  assert_eq(2 ** 3, 8, "exp")
})

// === COMPARISON OPERATORS ===

run("comparison operators", function() {
  assert_eq(5 == 5, true, "eq")
  assert_eq(5 != 6, true, "neq")
  assert_eq(3 < 5, true, "lt")
  assert_eq(5 > 3, true, "gt")
  assert_eq(3 <= 3, true, "lte")
  assert_eq(5 >= 5, true, "gte")
})

// === LOGICAL OPERATORS ===

run("logical operators", function() {
  assert_eq(true && true, true, "and")
  assert_eq(true && false, false, "and false")
  assert_eq(false || true, true, "or")
  assert_eq(false || false, false, "or false")
  assert_eq(!true, false, "not")
  assert_eq(!false, true, "not false")
})

run("short circuit", function() {
  var called = false
  var fn = function() { called = true; return true }
  var r = false && fn()
  assert_eq(called, false, "and short circuit")
  r = true || fn()
  assert_eq(called, false, "or short circuit")
})

// === BITWISE OPERATORS ===

run("bitwise operators", function() {
  assert_eq(5 & 3, 1, "and")
  assert_eq(5 | 3, 7, "or")
  assert_eq(5 ^ 3, 6, "xor")
  assert_eq(~0, -1, "not")
  assert_eq(1 << 3, 8, "lshift")
  assert_eq(8 >> 3, 1, "rshift")
  assert_eq(-1 >>> 1, 2147483647, "unsigned rshift")
})

// === UNARY OPERATORS ===

run("unary operators", function() {
  assert_eq(+5, 5, "unary plus")
  assert_eq(-5, -5, "unary minus")
  assert_eq(-(-5), 5, "double negate")
})

run("increment decrement", function() {
  var x = 5
  assert_eq(x++, 5, "postfix inc returns old")
  assert_eq(x, 6, "postfix inc side effect")
  x = 5
  assert_eq(++x, 6, "prefix inc returns new")
  x = 5
  assert_eq(x--, 5, "postfix dec returns old")
  assert_eq(x, 4, "postfix dec side effect")
  x = 5
  assert_eq(--x, 4, "prefix dec returns new")
})

// === COMPOUND ASSIGNMENT ===

run("compound assignment", function() {
  var x = 10
  x += 3; assert_eq(x, 13, "+=")
  x -= 3; assert_eq(x, 10, "-=")
  x *= 2; assert_eq(x, 20, "*=")
  x /= 4; assert_eq(x, 5, "/=")
  x %= 3; assert_eq(x, 2, "%=")
})

// === TERNARY OPERATOR ===

run("ternary operator", function() {
  var a = true ? 1 : 2
  assert_eq(a, 1, "ternary true")
  var b = false ? 1 : 2
  assert_eq(b, 2, "ternary false")
  var c = true ? (false ? 1 : 2) : 3
  assert_eq(c, 2, "ternary nested")
})

// === COMMA OPERATOR ===

run("comma operator", function() {
  var x = (1, 2, 3)
  assert_eq(x, 3, "comma returns last")
})

// === IN OPERATOR ===

run("in operator", function() {
  var o = {a: 1}
  assert_eq("a" in o, true, "key exists")
  assert_eq("b" in o, false, "key missing")
})

// === DELETE OPERATOR ===

run("delete operator", function() {
  var o = {a: 1, b: 2}
  delete o.a
  assert_eq("a" in o, false, "delete removes key")
  assert_eq(o.b, 2, "delete leaves others")
})

// === PROPERTY ACCESS ===

run("dot access", function() {
  var o = {x: 10}
  assert_eq(o.x, 10, "dot read")
  o.x = 20
  assert_eq(o.x, 20, "dot write")
})

run("bracket access", function() {
  var o = {x: 10}
  assert_eq(o["x"], 10, "bracket read")
  var key = "x"
  assert_eq(o[key], 10, "computed bracket")
  o["y"] = 20
  assert_eq(o.y, 20, "bracket write")
})

run("object-as-key", function() {
  var k = {}
  var o = {}
  o[k] = 42
  assert_eq(o[k], 42, "object key set/get")
  assert_eq(o[{}], null, "new object is different key")
  assert_eq(k in o, true, "object key in")
  delete o[k]
  assert_eq(k in o, false, "object key delete")
})

run("chained access", function() {
  var d = {a: {b: [1, {c: 99}]}}
  assert_eq(d.a.b[1].c, 99, "mixed chain")
})

// === ARRAY PUSH/POP SYNTAX ===

run("array push pop", function() {
  var a = [1, 2]
  a[] = 3
  assert_eq(length(a), 3, "push length")
  assert_eq(a[2], 3, "push value")
  var v = a[]
  assert_eq(v, 3, "pop value")
  assert_eq(length(a), 2, "pop length")
})

// === CONTROL FLOW: IF/ELSE ===

run("if else", function() {
  var x = 0
  if (true) x = 1
  assert_eq(x, 1, "if true")
  if (false) x = 2 else x = 3
  assert_eq(x, 3, "if else")
  if (false) x = 4
  else if (true) x = 5
  else x = 6
  assert_eq(x, 5, "else if")
})

// === CONTROL FLOW: WHILE ===

run("while loop", function() {
  var i = 0
  while (i < 5) i++
  assert_eq(i, 5, "while basic")
})

run("while break continue", function() {
  var i = 0
  while (true) {
    if (i >= 3) break
    i++
  }
  assert_eq(i, 3, "while break")
  var sum = 0
  i = 0
  while (i < 5) {
    i++
    if (i % 2 == 0) continue
    sum += i
  }
  assert_eq(sum, 9, "while continue")
})

// === CONTROL FLOW: FOR ===

run("for loop", function() {
  var sum = 0
  var i = 0
  for (i = 0; i < 5; i++) sum += i
  assert_eq(sum, 10, "for basic")
})

run("for break continue", function() {
  var sum = 0
  var i = 0
  for (i = 0; i < 10; i++) {
    if (i == 5) break
    sum += i
  }
  assert_eq(sum, 10, "for break")
  sum = 0
  for (i = 0; i < 5; i++) {
    if (i % 2 == 0) continue
    sum += i
  }
  assert_eq(sum, 4, "for continue")
})

run("nested for", function() {
  var sum = 0
  var i = 0, j = 0
  for (i = 0; i < 3; i++)
    for (j = 0; j < 3; j++)
      sum++
  assert_eq(sum, 9, "nested for")
})

// === FUNCTIONS ===

run("function expression", function() {
  var fn = function(a, b) { return a + b }
  assert_eq(fn(2, 3), 5, "basic call")
})

run("arrow function", function() {
  var double = x => x * 2
  assert_eq(double(5), 10, "arrow single param")
  var add = (a, b) => a + b
  assert_eq(add(2, 3), 5, "arrow multi param")
  var block = x => {
    var y = x * 2
    return y + 1
  }
  assert_eq(block(5), 11, "arrow block body")
})

run("function no return", function() {
  var fn = function() { var x = 1 }
  assert_eq(fn(), null, "no return gives null")
})

run("function early return", function() {
  var fn = function() { return 1; return 2 }
  assert_eq(fn(), 1, "early return")
})

run("extra and missing args", function() {
  var fn = function(a, b) { return a + b }
  assert_eq(fn(1, 2, 3), 3, "extra args ignored")
  var fn2 = function(a, b) { return a }
  assert_eq(fn2(1), 1, "missing args ok")
})

run("iife", function() {
  var r = (function(x) { return x * 2 })(21)
  assert_eq(r, 42, "immediately invoked")
})

// === CLOSURES ===

run("closure", function() {
  var make = function(x) {
    return function(y) { return x + y }
  }
  var add5 = make(5)
  assert_eq(add5(3), 8, "closure captures")
})

run("closure mutation", function() {
  var counter = function() {
    var n = 0
    return function() { n = n + 1; return n }
  }
  var c = counter()
  assert_eq(c(), 1, "first")
  assert_eq(c(), 2, "second")
})

// === RECURSION ===

run("recursion", function() {
  var fact = function(n) {
    if (n <= 1) return 1
    return n * fact(n - 1)
  }
  assert_eq(fact(5), 120, "factorial")
})

// === THIS BINDING ===

run("this binding", function() {
  var obj = {
    val: 10,
    get: function() { return this.val }
  }
  assert_eq(obj.get(), 10, "method this")
})

// === DISRUPTION ===

run("disrupt keyword", function() {
  assert_eq(should_disrupt(function() { disrupt }), true, "bare disrupt")
})

run("disruption handler", function() {
  var x = 0
  var fn = function() { x = 1 } disruption { x = 2 }
  fn()
  assert_eq(x, 1, "no disruption path")
  var fn2 = function() { disrupt } disruption { x = 3 }
  fn2()
  assert_eq(x, 3, "disruption caught")
})

run("disruption re-raise", function() {
  var outer_caught = false
  var outer = function() {
    var inner = function() { disrupt } disruption { disrupt }
    inner()
  } disruption {
    outer_caught = true
  }
  outer()
  assert_eq(outer_caught, true, "re-raise propagates")
})

// === PROTOTYPAL INHERITANCE ===

run("meme and proto", function() {
  var parent = {x: 10}
  var child = meme(parent)
  assert_eq(child.x, 10, "inherited prop")
  assert_eq(proto(child), parent, "proto returns parent")
  child.x = 20
  assert_eq(parent.x, 10, "override does not mutate parent")
})

run("meme with mixins", function() {
  var p = {a: 1}
  var m1 = {b: 2}
  var m2 = {c: 3}
  var child = meme(p, [m1, m2])
  assert_eq(child.a, 1, "parent prop")
  assert_eq(child.b, 2, "mixin1")
  assert_eq(child.c, 3, "mixin2")
})

// === STONE (FREEZE) ===

run("stone", function() {
  var o = {x: 1}
  assert_eq(is_stone(o), false, "not frozen")
  stone(o)
  assert_eq(is_stone(o), true, "frozen")
  assert_eq(should_disrupt(function() { o.x = 2 }), true, "write disrupts")
})

// === FUNCTION PROXY ===

run("function proxy", function() {
  var proxy = function(name, args) {
    return `${name}:${length(args)}`
  }
  assert_eq(proxy.hello(), "hello:0", "proxy dot call")
  assert_eq(proxy.add(1, 2), "add:2", "proxy with args")
  assert_eq(proxy["method"](), "method:0", "proxy bracket call")
  var m = "dynamic"
  assert_eq(proxy[m](), "dynamic:0", "proxy computed name")
})

run("non-proxy function prop access disrupts", function() {
  var fn = function() { return 1 }
  assert_eq(should_disrupt(function() { var x = fn.foo }), true, "prop read disrupts")
  assert_eq(should_disrupt(function() { fn.foo = 1 }), true, "prop write disrupts")
})

// === TYPE CHECKING ===

run("is_* functions", function() {
  assert_eq(is_number(42), true, "is_number")
  assert_eq(is_text("hi"), true, "is_text")
  assert_eq(is_logical(true), true, "is_logical")
  assert_eq(is_object({}), true, "is_object")
  assert_eq(is_array([]), true, "is_array")
  assert_eq(is_function(function(){}), true, "is_function")
  assert_eq(is_null(null), true, "is_null")
  assert_eq(is_object([]), false, "array not object")
  assert_eq(is_array({}), false, "object not array")
})

// === TRUTHINESS / FALSINESS ===

run("falsy values", function() {
  if (false) fail("false")
  if (0) fail("0")
  if ("") fail("empty string")
  if (null) fail("null")
  assert_eq(true, true, "all falsy passed")
})

run("truthy values", function() {
  if (!1) fail("1")
  if (!"hi") fail("string")
  if (!{}) fail("object")
  if (![]) fail("array")
  if (!true) fail("true")
  assert_eq(true, true, "all truthy passed")
})

// === VARIABLE SHADOWING ===

run("variable shadowing", function() {
  var x = 10
  var fn = function() {
    var x = 20
    return x
  }
  assert_eq(fn(), 20, "inner shadows")
  assert_eq(x, 10, "outer unchanged")
})

// === OPERATOR PRECEDENCE ===

run("precedence", function() {
  assert_eq(2 + 3 * 4, 14, "mul before add")
  assert_eq((2 + 3) * 4, 20, "parens override")
  assert_eq(-2 * 3, -6, "unary before mul")
})

// === CURRYING / HIGHER-ORDER ===

run("curried function", function() {
  var f = function(a) {
    return function(b) {
      return function(c) { return a + b + c }
    }
  }
  assert_eq(f(1)(2)(3), 6, "triple curry")
})

// === SELF-REFERENCING STRUCTURES ===

run("self-referencing object", function() {
  var o = {name: "root"}
  o.self = o
  assert_eq(o.self.self.name, "root", "cycle access")
})

// === IDENTIFIER ? AND ! ===

run("question mark in identifier", function() {
  var nil? = (x) => x == null
  assert_eq(nil?(null), true, "nil? null")
  assert_eq(nil?(42), false, "nil? 42")
})

run("bang in identifier", function() {
  var set! = (x) => x + 1
  assert_eq(set!(5), 6, "set! call")
})

run("question mark mid identifier", function() {
  var is?valid = (x) => x > 0
  assert_eq(is?valid(3), true, "is?valid true")
  assert_eq(is?valid(-1), false, "is?valid false")
})

run("bang mid identifier", function() {
  var do!stuff = () => 42
  assert_eq(do!stuff(), 42, "do!stuff call")
})

run("ternary after question ident", function() {
  var nil? = (x) => x == null
  var a = nil?(null) ? "yes" : "no"
  assert_eq(a, "yes", "ternary true branch")
  var b = nil?(42) ? "yes" : "no"
  assert_eq(b, "no", "ternary false branch")
})

run("bang not confused with logical not", function() {
  assert_eq(!true, false, "logical not true")
  assert_eq(!false, true, "logical not false")
})

run("inequality not confused with bang ident", function() {
  assert_eq(1 != 2, true, "inequality true")
  assert_eq(1 != 1, false, "inequality false")
})

// === SUMMARY ===

print(text(passed) + " passed, " + text(failed) + " failed out of " + text(passed + failed))
var _j = 0
if (failed > 0) {
  print("")
  for (_j = 0; _j < failed; _j++) {
    print("  FAIL " + error_names[_j] + ": " + error_reasons[_j])
  }
}
