function generateTestCase(assign) {
  let sideEffectCount = 0
  let patternCount = 0
  let limit = 10
  let depth = 0

  function choice(n) {
    return Math.random() * n | 0
  }

  function patternAndValue() {
    patternCount++
    switch (choice(3)) {
      case 0: return array()
      case 1: return object()
      case 2: return [assign(), choice(10), choice(10)]
    }
  }

  function sideEffect(result) {
    return `s(${sideEffectCount++}${result ? `, ${result}` : ''})`
  }

  function indent(open, items, close) {
    let tab = '  '
    items = items.map(i => `\n${tab.repeat(depth + 1)}${i}`).join(',')
    return `${open}${items}\n${tab.repeat(depth)}${close}`
  }

  function id() {
    return String.fromCharCode('a'.charCodeAt(0) + choice(3))
  }

  function array() {
    let count = 1 + choice(2)
    let pattern = []
    let value = []

    depth++
    for (let i = 0; i < count; i++) {
      if (patternCount > limit) break
      let [pat, val, defVal] = patternAndValue()
      switch (choice(3)) {
        case 0:
          pattern.push(pat)
          value.push(val)
          break
        case 1:
          pattern.push(`${pat} = ${sideEffect(defVal)}`)
          value.push(val)
          break
        case 2:
          pattern.push(`${pat} = ${sideEffect(defVal)}`)
          value.push(defVal)
          break
      }
      if (choice(10) < 8) value.push(val)
    }
    if (choice(2)) {
      pattern.push(`...${assign()}`)
      if (choice(10) < 8) value.push(choice(10))
    }
    depth--

    return [
      indent('[', pattern, ']'),
      indent('[', value, ']'),
      '[]',
    ]
  }

  function object() {
    let count = 1 + choice(2)
    let pattern = []
    let value = []
    let valKeys = new Set()

    depth++
    for (let i = 0; i < count; i++) {
      if (patternCount > limit) break
      let valKey = id()
      if (valKeys.has(valKey)) continue
      valKeys.add(valKey)
      let patKey = choice() ? valKey : `[${sideEffect(`'${valKey}'`)}]`
      let [pat, val, defVal] = patternAndValue()
      switch (choice(3)) {
        case 0:
          pattern.push(`${patKey}: ${pat}`)
          value.push(`${valKey}: ${val}`)
          break
        case 1:
          pattern.push(`${patKey}: ${pat} = ${sideEffect(defVal)}`)
          value.push(`${valKey}: ${val}`)
          break
        case 2:
          pattern.push(`${patKey}: ${pat} = ${sideEffect(defVal)}`)
          value.push(`${valKey}: ${defVal}`)
          break
      }
    }
    if (choice(2)) {
      pattern.push(`...${assign()}`)
      if (choice(10) < 8) value.push(`${id()}: ${choice(10)}`)
    }
    depth--

    return [
      indent('{', pattern, '}'),
      indent('{', value, '}'),
      '{}',
    ]
  }

  return choice(2) ? array() : object()
}

function evaluate(code) {
  let effectTrace = []
  let assignTarget = {}
  let sideEffect = (id, value) => (effectTrace.push(id), value)
  new Function('a', 's', code)(assignTarget, sideEffect)
  return JSON.stringify({ assignTarget, effectTrace })
}

function generateTestCases(trials) {
  let testCases = []

  while (testCases.length < trials) {
    let ids = []
    let assignCount = 0
    let [pattern, value] = generateTestCase(() => {
      let id = `_${assignCount++}`
      ids.push(id)
      return id
    })
    try {
      evaluate(`(${pattern.replace(/_/g, 'a._')} = ${value});`)
      testCases.push([pattern, value, ids])
    } catch (e) {
    }
  }

  return testCases
}

function AssignmentOperator([pattern, value]) {
  let ts = `(${pattern.replace(/_/g, 'a._')} = ${value});`
  let js = ts
  return { js, ts }
}

function NamespaceExport([pattern, value]) {
  let ts = `namespace a { export const ${`${pattern} = ${value}`} }`
  let js = `(${pattern.replace(/_/g, 'a._')} = ${value});`
  return { js, ts }
}

function ConstDeclaration([pattern, value, ids]) {
  let ts = `const ${pattern} = ${value};${ids.map(id => `\na.${id} = ${id};`).join('')}`
  let js = ts
  return { js, ts }
}

function LetDeclaration([pattern, value, ids]) {
  let ts = `let ${pattern} = ${value};${ids.map(id => `\na.${id} = ${id};`).join('')}`
  let js = ts
  return { js, ts }
}

function VarDeclaration([pattern, value, ids]) {
  let ts = `var ${pattern} = ${value};${ids.map(id => `\na.${id} = ${id};`).join('')}`
  let js = ts
  return { js, ts }
}

function TryCatchBinding([pattern, value, ids]) {
  let ts = `try { throw ${value} } catch (${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }`
  let js = ts
  return { js, ts }
}

function FunctionStatementArguments([pattern, value, ids]) {
  let ts = `function foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }\nfoo(${value});`
  let js = ts
  return { js, ts }
}

function FunctionExpressionArguments([pattern, value, ids]) {
  let ts = `(function(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} })(${value});`
  let js = ts
  return { js, ts }
}

function ArrowFunctionArguments([pattern, value, ids]) {
  let ts = `((${pattern}) => { ${ids.map(id => `a.${id} = ${id};`).join('\n')} })(${value});`
  let js = ts
  return { js, ts }
}

function ObjectMethodArguments([pattern, value, ids]) {
  let ts = `({ foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} } }).foo(${value});`
  let js = ts
  return { js, ts }
}

function ClassStatementMethodArguments([pattern, value, ids]) {
  let ts = `class Foo { foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} } }\nnew Foo().foo(${value});`
  let js = ts
  return { js, ts }
}

function ClassExpressionMethodArguments([pattern, value, ids]) {
  let ts = `(new (class { foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} } })).foo(${value});`
  let js = ts
  return { js, ts }
}

function ForLoopConst([pattern, value, ids]) {
  let ts = `var i; for (const ${pattern} = ${value}; i < 1; i++) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }`
  let js = ts
  return { js, ts }
}

function ForLoopLet([pattern, value, ids]) {
  let ts = `for (let ${pattern} = ${value}, i = 0; i < 1; i++) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }`
  let js = ts
  return { js, ts }
}

function ForLoopVar([pattern, value, ids]) {
  let ts = `for (var ${pattern} = ${value}, i = 0; i < 1; i++) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }`
  let js = ts
  return { js, ts }
}

function ForLoop([pattern, value]) {
  let ts = `for (${pattern.replace(/_/g, 'a._')} = ${value}; 0; ) ;`
  let js = ts
  return { js, ts }
}

function ForOfLoopConst([pattern, value, ids]) {
  let ts = `for (const ${pattern} of [${value}]) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }`
  let js = ts
  return { js, ts }
}

function ForOfLoopLet([pattern, value, ids]) {
  let ts = `for (let ${pattern} of [${value}]) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }`
  let js = ts
  return { js, ts }
}

function ForOfLoopVar([pattern, value, ids]) {
  let ts = `for (var ${pattern} of [${value}]) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }`
  let js = ts
  return { js, ts }
}

function ForOfLoop([pattern, value]) {
  let ts = `for (${pattern.replace(/_/g, 'a._')} of [${value}]) ;`
  let js = ts
  return { js, ts }
}

async function verify(test, transform, testCases) {
  let verbose = process.argv.indexOf('--verbose') >= 0
  let indent = t => t.replace(/\n/g, '\n  ')
  let newline = false
  console.log(`${test.name} (${transform.name}):`)

  await concurrentMap(testCases, 20, async (testCase) => {
    let { js, ts } = test(testCase)
    let expected
    try {
      expected = evaluate(js)
    } catch (e) {
      return
    }

    let transformed
    try {
      transformed = await transform(ts)
    } catch (e) {
      process.stdout.write('T')
      newline = true

      if (verbose) {
        console.log('\n' + '='.repeat(80))
        console.log(indent(`Original code:\n${ts}`))
        console.log(indent(`Transform error:\n${e}`))
        newline = false
      }
      return
    }

    let actual
    try {
      actual = evaluate(transformed)
    } catch (e) {
      actual = e + ''
    }

    if (actual !== expected) {
      process.stdout.write(actual.indexOf('SyntaxError') >= 0 ? 'S' : 'X')
      newline = true

      if (verbose) {
        console.log('\n' + '='.repeat(80))
        console.log(indent(`Original code:\n${ts}`))
        console.log(indent(`Transformed code:\n${transformed}`))
        console.log(indent(`Expected output:\n${expected}`))
        console.log(indent(`Actual output:\n${actual}`))
        newline = false
      }
    } else {
      process.stdout.write('-')
      newline = true
    }
  })

  if (newline) process.stdout.write('\n')
}

function concurrentMap(items, batch, callback) {
  return new Promise((resolve, reject) => {
    let index = 0
    let pending = 0
    let next = () => {
      if (index === items.length && pending === 0) {
        resolve()
      } else if (index < items.length) {
        let item = items[index++]
        pending++
        callback(item).then(() => {
          pending--
          next()
        }, e => {
          items.length = 0
          reject(e)
        })
      }
    }
    for (let i = 0; i < batch; i++)next()
  })
}

async function main() {
  let es = require('./esbuild').installForTests()
  let esbuild = async (x) => (await es.transform(x, { target: 'es6', loader: 'ts' })).code.trim()

  console.log(`
Options:
  --verbose = Print details for failures

Legend:
  - = The test passed
  X = The test failed
  T = The transform function itself failed
  S = The generated code has a syntax error`)

  let tests = [
    // Bindings
    ConstDeclaration,
    LetDeclaration,
    VarDeclaration,
    TryCatchBinding,
    FunctionStatementArguments,
    FunctionExpressionArguments,
    ArrowFunctionArguments,
    ObjectMethodArguments,
    ClassStatementMethodArguments,
    ClassExpressionMethodArguments,
    ForLoopConst,
    ForLoopLet,
    ForLoopVar,
    ForOfLoopConst,
    ForOfLoopLet,
    ForOfLoopVar,

    // Destructuring
    AssignmentOperator,
    ForLoop,
    ForOfLoop,

    // TypeScript-specific
    NamespaceExport,
  ]
  let transforms = [
    esbuild,
  ]
  let testCases = generateTestCases(100)

  for (let transform of transforms) {
    console.log()
    for (let test of tests) {
      await verify(test, transform, testCases)
    }
  }
}

main().catch(e => setTimeout(() => { throw e }))
