import { setTimeout, setImmediate } from 'timers/promises'
import { Eltro as t, assert, stub } from 'eltro'
import fs from 'fs/promises'
import Application from '../core/application.mjs'
import Util from '../core/util.mjs'
import StaticProvider from '../core/providers/static.mjs'
import { createFakeContext } from './helpers.mjs'

import bunyan from 'bunyan-lite'
import HttpServer from '../core/http.mjs'
import { request } from '../core/client.mjs'
import getLog from '../core/log.mjs'

const util = new Util(import.meta.url)

const logger = {
  info: stub(),
  warn: stub(),
  error: stub(),
}
function createProvider() {
  return {
    getLatestVersion: stub(),
    downloadVersion: stub(),
  }
}

t.describe('constructor()', function() {
  let ctx

  t.beforeEach(function() {
    return createFakeContext()
      .then(function(res) { ctx = res })
  })

  t.test('should auto-create application', function() {
    assert.notOk(ctx.db.data.core.test)

    new Application(ctx, {}, 'test')

    assert.ok(ctx.db.data.core.test)
    assert.ok(ctx.db.data.core.test.versions)
    assert.strictEqual(ctx.db.data.core.test.active, '')
    assert.strictEqual(ctx.db.data.core.test.latestInstalled, '')
  })

  t.test('should keep config and other of itself', function() {
    const assertTest = { a: 1 }
    const assertName = 'test'
    ctx.db.config = {
      test: assertTest,
      app: { b: 2},
      manage: { c: 3 },
    }

    let app = new Application(ctx, {}, assertName)
    assert.notStrictEqual(app.config, assertTest)
    assert.strictEqual(app.config.a, assertTest.a)
    assert.strictEqual(app.config.updateEvery, 180)
    assert.strictEqual(app.config.startWaitUntilFail, 60 * 1000)
    assert.strictEqual(app.config.heartbeatTimeout, 3 * 1000)
    assert.strictEqual(app.config.heartbeatAttempts, 5)
    assert.strictEqual(app.config.heartbeatAttemptsWait, 2 * 1000)
    assert.strictEqual(app.config.heartbeatPath, '/')
    assert.strictEqual(app.config.clusterWaitOnCrash, 1 * 1000)
    assert.strictEqual(app.ctx.db, ctx.db)
    assert.strictEqual(app.ctx.app, app)
    assert.strictEqual(app.ctx.config, app.config)
    assert.strictEqual(app.ctx.util, ctx.util)
    assert.strictEqual(app.ctx.sc.Util, Util)
    assert.strictEqual(app.ctx.sc.bunyan, bunyan)
    assert.strictEqual(app.ctx.sc.HttpServer, HttpServer)
    assert.strictEqual(app.ctx.sc.request, request)
    assert.strictEqual(app.ctx.sc.getLog, getLog)
    assert.strictEqual(app.name, assertName)
    assert.strictEqual(app.fresh, true)
    assert.strictEqual(app.ctx.version, '')
    assert.strictEqual(app.monitoringCluster, false)
    assert.deepStrictEqual(app.workers, {})
    assert.strictEqual(app.isSlave, false)
    assert.ok(app.http)
    assert.ok(app.http.sockets)
    assert.strictEqual(typeof(app.http.createServer), 'function')
    assert.strictEqual(typeof(app.http.closeServer), 'function')
  })

  t.test('should support overriding defaults', function() {
    const assertTest = {
      a: 1,
      updateEvery: 10,
      startWaitUntilFail: 10,
      heartbeatTimeout: 10,
      heartbeatAttempts: 10,
      heartbeatAttemptsWait: 10,
      heartbeatPath: '/asdf',
      clusterWaitOnCrash: 10,
    }
    const assertName = 'test'
    ctx.db.config = {
      test: assertTest,
      app: { b: 2},
      manage: { c: 3 },
    }

    let app = new Application(ctx, {}, assertName)
    assert.notStrictEqual(app.config, assertTest)
    assert.strictEqual(app.config.a, assertTest.a)
    assert.strictEqual(app.config.updateEvery, 10)
    assert.strictEqual(app.config.startWaitUntilFail, 10)
    assert.strictEqual(app.config.heartbeatTimeout, 10)
    assert.strictEqual(app.config.heartbeatAttempts, 10)
    assert.strictEqual(app.config.heartbeatAttemptsWait, 10)
    assert.strictEqual(app.config.heartbeatPath, '/asdf')
    assert.strictEqual(app.config.clusterWaitOnCrash, 10)
  })

  t.test('should fill out workers indexes if is master', function() {
    const assertTest = { a: 1, cluster: 2 }
    const assertName = 'test'
    ctx.db.config = {
      test: assertTest,
      app: { b: 2},
      manage: { c: 3 },
    }

    let app = new Application(ctx, {}, assertName)
    assert.notStrictEqual(app.config, assertTest)
    assert.strictEqual(app.config.a, assertTest.a)
    assert.deepStrictEqual(app.workers, {
      1: null,
      2: null,
    })
  })

  t.test('should leave workers empty if not master', function() {
    const assertTest = { a: 1, cluster: 2 }
    const assertName = 'test'
    ctx.db.config = {
      test: assertTest,
      app: { b: 2},
      manage: { c: 3 },
    }

    let app = new Application(ctx, {}, assertName, { cluster: { isWorker: true } })
    assert.notStrictEqual(app.config, assertTest)
    assert.strictEqual(app.config.a, assertTest.a)
    assert.deepStrictEqual(app.workers, {})
  })

  t.test('should not default updateEvery if its zero', function() {
    const assertTest = { a: 1, updateEvery: 0 }
    const assertName = 'test'
    ctx.db.config = {
      test: assertTest,
      app: { b: 2},
      manage: { c: 3 },
    }

    let app = new Application(ctx, {}, assertName)
    assert.notStrictEqual(app.config, assertTest)
    assert.strictEqual(app.config.a, assertTest.a)
    assert.strictEqual(app.config.updateEvery, assertTest.updateEvery)
    assert.strictEqual(app.config.updateEvery, 0)
  })

  t.test('should create http instance correctly', function() {
    ctx.db.config = {
      testapp: { a: 1, https: true },
      app: { b: 2},
      manage: { c: 3 },
    }

    let app = new Application(ctx, {}, 'testapp')
    assert.ok(app.http)
    assert.ok(app.http.ishttps)
  })

  t.test('should keep provider', function() {
    const assertProvider = { a: 1 }
    let app = new Application(ctx, assertProvider, 'test')
    assert.strictEqual(app.provider, assertProvider)
  })
})

t.describe('#startAutoupdater()', function() {
  let ctx

  t.beforeEach(function() {
    return createFakeContext()
      .then(function(res) { ctx = res })
  })
  
  t.test('should do nothing if provider is static', async function() {
    const stubInterval = stub()
    stubInterval.throws(new Error('should not be seen'))
    let provider = new StaticProvider()
    let app = new Application(ctx, provider, 'teststatic', { setInterval: stubInterval })

    app.startAutoupdater()
  })
  
  t.test('should do nothing if isSlave but should warn', async function() {
    const stubInterval = stub()
    stubInterval.throws(new Error('should not be seen'))
    let app = new Application(ctx, { }, 'teststatic', { setInterval: stubInterval })
    app.ctx.log.warn.reset()
    assert.notOk(app.ctx.log.warn.called)
    app.isSlave = true

    app.startAutoupdater()

    assert.ok(app.ctx.log.warn.called)
    assert.match(app.ctx.log.warn.firstCall[0], /slave/i)
  })
  
  t.test('should do nothing if updateEvery is zero', async function() {
    const stubInterval = stub()
    stubInterval.throws(new Error('should not be seen'))
    ctx.db.config.test = { updateEvery: 0 }
    let app = new Application(ctx, { }, 'test', { setInterval: stubInterval })

    app.startAutoupdater()
  })

  t.test('should call setInterval correctly', function() {
    const assertTimeMinutes = 1440
    const stubInterval = stub()
    const stubUnref = stub()
    stubInterval.returns({ unref: stubUnref })

    ctx.db.config.test = {
      updateEvery: assertTimeMinutes,
    }

    let app = new Application(ctx, {}, 'test', { setInterval: stubInterval })

    assert.strictEqual(stubInterval.called, false)
    assert.strictEqual(stubUnref.called, false)

    app.startAutoupdater()
    
    assert.strictEqual(stubInterval.called, true)
    assert.strictEqual(stubUnref.called, true)
    assert.strictEqual(typeof(stubInterval.firstCall[0]), 'function')
    assert.strictEqual(stubInterval.firstCall[1], assertTimeMinutes * 60 * 1000)
  })

  t.test('should support default value if updateEvery is not defined', function() {
    const stubInterval = stub()
    stubInterval.returns({ unref: function() {} })

    let app = new Application(ctx, {}, 'test', { setInterval: stubInterval })

    assert.strictEqual(stubInterval.called, false)

    app.startAutoupdater()
    
    assert.strictEqual(stubInterval.called, true)
    assert.strictEqual(typeof(stubInterval.firstCall[0]), 'function')
    assert.strictEqual(stubInterval.firstCall[1], 3 * 60 * 60 * 1000)
  })

  t.test('should call update as promise correctly', async function() {
    const stubUpdate = stub()
    const stubInterval = stub()
    stubInterval.returns({ unref: function() {} })

    stubUpdate.returnWith(function() {
      return Promise.resolve()
    })

    let app = new Application(ctx, {}, 'test', { setInterval: stubInterval })
    app.update = stubUpdate
    app.startAutoupdater()

    assert.strictEqual(typeof(stubInterval.firstCall[0]), 'function')
    assert.notStrictEqual(stubInterval.firstCall, stubUpdate)

    stubInterval.firstCall[0]()

    while (ctx.db.data.core.test.updater === '') {
      await setTimeout(10)
    }

    assert.match(ctx.db.data.core.test.updater, /auto/i)
    assert.match(ctx.db.data.core.test.updater, /update/i)
  })

  t.test('should add any errors to last in db update check on errors when updating', async function() {
    const stubInterval = stub()
    const assertErrorMessage = 'Ai Do'
    stubInterval.returns({ unref: function() {} })

    let app = new Application(ctx, {}, 'test', { setInterval: stubInterval })
    app.update = function() {
      return Promise.reject(new Error(assertErrorMessage))
    }
    app.startAutoupdater()

    assert.strictEqual(ctx.db.data.core.test.updater, '')

    stubInterval.firstCall[0]()

    while (ctx.db.data.core.test.updater === '') {
      await setTimeout(10)
    }

    assert.match(ctx.db.data.core.test.updater, /auto/i)
    assert.match(ctx.db.data.core.test.updater, /update/i)
    assert.match(ctx.db.data.core.test.updater, new RegExp(assertErrorMessage))
  })
})

t.describe('#closeServer()', function() {
  let app
  let cluster
  let stubCloseServer

  t.beforeEach(function() {
    cluster = {
      off: stub(),
    }
    return createFakeContext()
      .then(function(res) {
        let provider = createProvider()
        app = new Application(res, provider, 'testapp', { cluster: cluster })
        app.http.closeServer = stubCloseServer = stub().resolves()
      })
  })

  t.test('should call closeServer correctly', async function() {
    const assertNotVersion = 'v1521'
    const assertError = new Error('Moonlight Fiesta')
    stubCloseServer.rejects(assertError)

    app.ctx.version = assertNotVersion
    let err = await assert.isRejected(app.closeServer())
    assert.strictEqual(app.ctx.version, '')

    assert.strictEqual(err, assertError)
  })

  t.test('should kill entire cluster if cluster master', async function() {
    let workers = {}
    let handle = app.__clusterWorkerDied = function() {}
    app.config.cluster = Math.floor(Math.random() * (10 - 4 + 1) + 4)
    for (let i = 1; i <= app.config.cluster; i++) {
      if (i % 2 === 0) {
        workers[i] = app.workers[i] = {
          process: {
            kill: stub().returnWith(function() {
              assert.strictEqual(app.__clusterWorkerDied, null)
              assert.strictEqual(cluster.off.callCount, 1)
              assert.strictEqual(cluster.off.firstCall[0], 'exit')
              assert.strictEqual(cluster.off.firstCall[1], handle)
            })
          }
        }
      } else {
        workers[i] = app.workers[i] = null
      }
    }

    await app.closeServer()

    for (let i = 1; i <= app.config.cluster; i++) {
      if (workers[i]) {
        assert.ok(workers[i].process.kill.called)
        assert.strictEqual(app.__clusterWorkerDied, null)
      }
    }
  })

  t.test('otherwise should work fine', async function() {
    await app.closeServer()
  })
})

t.describe('#workerDied()', function() {
  let app
  let stubTimeout
  let stubFork
  
  t.beforeEach(function() {
    stubTimeout = stub()
    return createFakeContext()
      .then(function(res) {
        let provider = createProvider()
        app = new Application(res, provider, 'testapp', { setTimeout: stubTimeout })
        app.startForkProcess = stubFork = stub()
      })
  })

  t.test('should do nothing if worker is not found at index', function() {
    const assertWorkerId = 2
    const assertWorker = { a: 1, w_id: assertWorkerId }
    stubTimeout.throws(new Error('should not be seen'))
    stubFork.throws(new Error('should not be seen'))
    app.workers[1] = assertWorker

    app.workerDied(assertWorker)
  })

  t.test('should mark worker as null and call setTimeout', function() {
    const assertWorkerId = 2
    const assertTimeoutDuration = 12421
    const assertVersion = 'v11.22.33'
    const assertWorker = { a: 1, w_id: assertWorkerId }
    app.ctx.db.data.core[app.name].active = assertVersion
    app.workers[assertWorkerId] = assertWorker
    app.clusterWaitOnCrash = assertTimeoutDuration

    app.workerDied(assertWorker)

    assert.strictEqual(app.workers[assertWorkerId], null)
    assert.ok(stubTimeout.called)
    assert.strictEqual(typeof(stubTimeout.firstCall[0]), 'function')
    assert.strictEqual(stubTimeout.firstCall[1], assertTimeoutDuration)
    assert.notOk(stubFork.called)

    stubTimeout.firstCall[0]()

    assert.ok(stubFork.called)
    assert.ok(stubFork.firstCall[0], assertWorkerId)
    assert.ok(stubFork.firstCall[1], assertVersion)
  })

  t.test('setTimeout should not call startForkProcess if one was already started', function() {
    const assertWorkerId = 2
    const assertWorker = { a: 1, w_id: assertWorkerId }
    app.workers[assertWorkerId] = assertWorker

    app.workerDied(assertWorker)

    assert.strictEqual(app.workers[assertWorkerId], null)

    app.workers[assertWorkerId] = { a : 2 }

    stubTimeout.firstCall[0]()

    assert.notOk(stubFork.called)
  })
})

t.describe('#update()', function() {
  let ctx
  let app
  let provider
  let stubExtract
  let stubRunCommand
  let stubWrite
  let stubFsMkdir
  let stubFsRm
  let stubFsStat

  t.beforeEach(function() {
    return createFakeContext()
      .then(function(res) {
        ctx = res
        ctx.util.extractFile = stubExtract = stub().resolves()
        ctx.util.runCommand = stubRunCommand = stub().resolves()
        ctx.db.write = stubWrite = stub().resolves()

        provider = createProvider()
        app = new Application(ctx, provider, 'testapp', {
          fs: {
            mkdir: stubFsMkdir = stub(),
            rm: stubFsRm = stub(),
            stat: stubFsStat = stub(),
          },
        })
        stubFsMkdir.resolves()
        stubFsRm.resolves()
        stubFsStat.resolves({})
        provider.downloadVersion.resolves()
        provider.getLatestVersion.resolves({ version: '123456789', link: 'httplinkhere', filename: 'test.7z' })
      })
  })

  t.afterEach(function() {
    return fs.rm('./test/testapp/123456789', { force: true, recursive: true })
  })

  t.test('should do nothing if provider is static', async function() {
    provider = new StaticProvider()
    app = new Application(ctx, provider, 'teststatic')

    stubWrite.reset().resolves()

    let result = await app.update()

    assert.strictEqual(result, null)
    assert.match(ctx.db.data.core.teststatic.updater, /static/i)
    assert.match(ctx.db.data.core.teststatic.updater, /nothing/i)
    let old = ctx.db.data.core.teststatic.updater
    assert.ok(stubWrite.called)
    assert.strictEqual(stubWrite.callCount, 1)
    
    await app.update()
    assert.strictEqual(result, null)
    assert.strictEqual(ctx.db.data.core.teststatic.updater, old)
    assert.strictEqual(stubWrite.callCount, 1)

    ctx.db.data.core.teststatic.updater = 'asdf'
    
    await app.update()
    assert.strictEqual(result, null)
    assert.strictEqual(ctx.db.data.core.teststatic.updater, old)
    assert.strictEqual(stubWrite.callCount, 2)
    
    await app.update()
    assert.strictEqual(result, null)
    assert.strictEqual(ctx.db.data.core.teststatic.updater, old)
    assert.strictEqual(stubWrite.callCount, 2)
  })

  t.test('should do nothing if slave but warn', async function() {
    stubWrite.reset().resolves()

    app.isSlave = true
    app.ctx.log.warn.reset()
    assert.notOk(app.ctx.log.warn.called)

    let result = await app.update()

    assert.strictEqual(result, null)
    assert.notOk(ctx.db.data.core.teststatic)
    assert.notOk(stubWrite.called)

    assert.ok(app.ctx.log.warn.called)
    assert.match(app.ctx.log.warn.firstCall[0], /slave/i)
  })

  t.test('multiple calls should be safe', async function() {
    ctx.db.data.core.testapp.updater = ''

    provider.getLatestVersion.returnWith(function() {
      return new Promise(function() {})
    })

    assert.strictEqual(app.updating, false)

    app.update()
    await setImmediate()

    assert.strictEqual(app.updating, true)
    assert.strictEqual(provider.getLatestVersion.callCount, 1)

    let result = await app.update()
    await setImmediate()
    
    assert.strictEqual(provider.getLatestVersion.callCount, 1)
    assert.strictEqual(result, null)
  })

  t.test('should check for latest version', async function() {
    const assertError = new Error('Ore wa Subete wo Shihaisuru')
    provider.getLatestVersion.rejects(assertError)
    ctx.db.data.core.testapp.updater = ''

    let err = await assert.isRejected(app.update())
    
    assert.strictEqual(app.updating, false)
    assert.strictEqual(err, assertError)
    
    assert.ok(stubWrite.called)
    assert.ok(stubWrite.callCount >= 1)
    assert.match(ctx.db.data.core.testapp.updater, /check/i)
    assert.match(ctx.db.data.core.testapp.updater, /version/i)
    assert.match(ctx.db.data.core.testapp.updater, new RegExp(new Date().toISOString().split('T')[0]))
    assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertError.message))
  })

  t.test('should call provider download latest correctly if new 7zip version', async function() {
    const assertError = new Error('Without a fight')
    const assertLink = 'All of you'
    const assertVersion = { version: '123456789', link: assertLink, filename: 'test.7z' }
    const assertTarget = util.getPathFromRoot('./testapp/123456789/file.7z')
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0)

    provider.getLatestVersion.resolves(assertVersion)
    provider.downloadVersion.rejects(assertError)
    ctx.db.data.core.testapp.updater = ''

    let err = await assert.isRejected(app.update())
    
    assert.strictEqual(app.updating, false)
    assert.strictEqual(err, assertError)
    assert.match(ctx.db.data.core.testapp.updater, /found/i)
    assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertVersion.version))
    assert.match(ctx.db.data.core.testapp.updater, /downloading/i)
    assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertLink))
    assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertTarget.replace(/\\/g, '\\\\')))
    assert.strictEqual(provider.downloadVersion.firstCall[0], assertVersion)
    assert.strictEqual(provider.downloadVersion.firstCall[1], assertTarget)
    assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertError.message))
    
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1)
    assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion)
    assert.ok(assertVersion.log)
    assert.match(assertVersion.log, /\. \n/)
    assert.match(assertVersion.log, new RegExp(assertError.message))
    assert.ok(assertVersion.log.endsWith('\n'))
    assert.strictEqual(assertVersion.failtodownload, 1)
    
    assert.ok(stubWrite.called)
    assert.ok(stubWrite.callCount >= 2)
    assert.ok(stubFsMkdir.called)
    assert.match(stubFsMkdir.firstCall[0], /123456789/)
    assert.ok(stubFsMkdir.firstCall[1]?.recursive)

    // Test if subsequent calls increment counter

    err = await assert.isRejected(app.update())
    
    assert.strictEqual(app.updating, false)
    assert.strictEqual(err, assertError)
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1)
    assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion)
    assert.strictEqual(assertVersion.failtodownload, 2)
  })

  t.test('should call provider download latest correctly if new 7zip version', async function() {
    const assertError = new Error('Without a fight')
    const assertLink = 'All of you'
    const assertVersion = { version: '123456789', link: assertLink, filename: 'test.7z' }
    const assertTarget = util.getPathFromRoot('./testapp/123456789/file.7z')
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0)

    provider.getLatestVersion.resolves(assertVersion)
    provider.downloadVersion.rejects(assertError)
    ctx.db.data.core.testapp.updater = ''

    let err = await assert.isRejected(app.update())
    
    assert.strictEqual(app.updating, false)
    assert.strictEqual(err, assertError)
    assert.match(ctx.db.data.core.testapp.updater, /found/i)
    assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertVersion.version))
    assert.match(ctx.db.data.core.testapp.updater, /downloading/i)
    assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertLink))
    assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertTarget.replace(/\\/g, '\\\\')))
    assert.strictEqual(provider.downloadVersion.firstCall[0], assertVersion)
    assert.strictEqual(provider.downloadVersion.firstCall[1], assertTarget)
    assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertError.message))
    
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1)
    assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion)
    assert.ok(assertVersion.log)
    assert.match(assertVersion.log, /\. \n/)
    assert.match(assertVersion.log, new RegExp(assertError.message))
    assert.ok(assertVersion.log.endsWith('\n'))
    assert.strictEqual(assertVersion.failtodownload, 1)
    
    assert.ok(stubFsRm.called)
    assert.match(stubFsRm.firstCall[0], /123456789/)
    assert.ok(stubFsRm.firstCall[1]?.recursive)
    assert.ok(stubFsRm.firstCall[1]?.force)
    
    // Test if subsequent calls increment counter

    err = await assert.isRejected(app.update())
    
    assert.strictEqual(app.updating, false)
    assert.strictEqual(err, assertError)
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1)
    assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion)
    assert.strictEqual(assertVersion.failtodownload, 2)
  })

  t.test('should call extract on util correctly', async function() {
    const assertExtractText = 'Reverdations of Success'
    const assertError = new Error('Dai Gekisen')
    const assertTarget = util.getPathFromRoot('./testapp/123456789/file.7z')
    const assertFolder = util.getPathFromRoot('./testapp/123456789')

    stubExtract.returnWith(function(target, stream) {
      stream(assertExtractText)
      return Promise.reject(assertError)
    })
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0)
    
    let err = await assert.isRejected(app.update())

    assert.strictEqual(app.updating, false)
    assert.strictEqual(err, assertError)
    assert.strictEqual(app.updating, false)
    assert.strictEqual(stubExtract.firstCall[0], assertTarget)

    assert.match(ctx.db.data.core.testapp.updater, new RegExp('extracting[^.]+file\.7z', 'i'))
    assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertError.message))
    
    assert.ok(stubFsRm.called)
    assert.strictEqual(stubFsRm.firstCall[0], assertFolder)
    assert.ok(stubFsRm.firstCall[1]?.recursive)
    assert.ok(stubFsRm.firstCall[1]?.force)
    
    assert.ok(stubWrite.called)
    assert.ok(stubWrite.callCount >= 3)
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1)
    let version = ctx.db.data.core.testapp.versions[0]
    assert.ok(version.log)
    assert.match(version.log, /\. \n/)
    assert.match(version.log, new RegExp('extracting[^.]+file\.7z', 'i'))
    assert.match(version.log, new RegExp(assertError.message))
    assert.match(version.log, new RegExp(assertExtractText))
    assert.ok(version.log.endsWith('\n'))
    assert.strictEqual(version.failtodownload, 1)
    
    // Test if subsequent calls increment counter

    err = await assert.isRejected(app.update())
    
    assert.strictEqual(app.updating, false)
    assert.strictEqual(err, assertError)
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1)
    assert.strictEqual(ctx.db.data.core.testapp.versions[0], version)
    assert.strictEqual(version.failtodownload, 2)
  })

  t.test('should call fs remove the archieve file', async function() {
    const assertError = new Error('Tiny Kizuna')
    const assertTarget = util.getPathFromRoot('./testapp/123456789/file.7z')
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0)
    stubFsRm.rejects(assertError)

    await app.update()

    assert.strictEqual(app.updating, false)
    assert.ok(stubFsRm.called)
    assert.strictEqual(stubFsRm.callCount, 1)
    assert.strictEqual(stubFsRm.firstCall[0], assertTarget)
    assert.ok(stubFsRm.firstCall[1]?.force)
  })

  t.test('should not call npm install if stat fails to find index.mjs', async function() {
    const assertError = new Error('File not found')
    const assertTarget = util.getPathFromRoot('./testapp/123456789/index.mjs')
    const stubUpdated = stub()
    stubRunCommand.rejects(new Error('should not be seen'))
    stubFsStat.rejects(assertError)
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0)

    app.once('updated', stubUpdated)

    let err = await assert.isRejected(app.update())

    assert.strictEqual(app.updating, false)
    assert.ok(stubExtract.called)
    assert.strictEqual(err, assertError)
    assert.strictEqual(stubFsStat.firstCall[0], assertTarget)
    assert.match(ctx.db.data.core.testapp.updater, /index\.mjs/i)
    assert.match(ctx.db.data.core.testapp.updater, /missing/i)
    
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1)
    let version = ctx.db.data.core.testapp.versions[0]
    assert.ok(version.log)
    assert.match(version.log, /index\.mjs/i)
    assert.match(version.log, /missing/i)
    assert.ok(version.log.endsWith('\n'))
    assert.strictEqual(version.failtodownload, 1)
    
    // Test if subsequent calls increment counter

    err = await assert.isRejected(app.update())
    
    assert.notOk(stubUpdated.called)
    assert.strictEqual(app.updating, false)
    assert.strictEqual(err, assertError)
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1)
    assert.strictEqual(ctx.db.data.core.testapp.versions[0], version)
    assert.strictEqual(version.failtodownload, 2)
  })
  
  t.test('should not call npm install if package.json is missing but shuld pass', async function() {
    const assertError = new Error('File not found')
    const assertTarget = util.getPathFromRoot('./testapp/123456789/package.json')
    stubRunCommand.rejects(new Error('should not be seen'))
    stubFsStat.returnWith(function(path) {
      if (path.endsWith('package.json')) {
        return Promise.reject(assertError)
      }
      return Promise.resolve({})
    })
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0)

    let result = await new Promise(function(res, rej) {
      app.once('updated', res)
      app.update().catch(rej)
    })

    assert.strictEqual(app.updating, false)
    assert.strictEqual(stubFsStat.callCount, 2)
    assert.strictEqual(stubFsStat.secondCall[0], assertTarget)
    assert.ok(stubExtract.called)
    assert.notOk(stubRunCommand.called)
    assert.match(ctx.db.data.core.testapp.updater, /package\.json/i)
    assert.match(ctx.db.data.core.testapp.updater, /contain/i)
    
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1)
    let version = ctx.db.data.core.testapp.versions[0]
    assert.ok(version.log)
    assert.strictEqual(result, version)
    assert.match(version.log, /package\.json/i)
    assert.match(version.log, /contain/i)
    assert.ok(version.log.endsWith('\n'))
    assert.ok(ctx.db.data.core.testapp.latestInstalled)
    assert.match(version.log, /finished/i)
    assert.match(version.log, /updating/i)
    
    // Test if subsequent calls do nothing

    provider.downloadVersion.reset()

    result = await app.update()

    assert.strictEqual(result, null)
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1)
    assert.notOk(provider.downloadVersion.called)
    assert.match(ctx.db.data.core.testapp.updater, /already/i)
    assert.match(ctx.db.data.core.testapp.updater, /nothing/i)
  })

  t.test('should otherwise call npm install correctly', async function() {
    const assertExtractText = 'Egao no Hikair ni Tsutsumarete'
    const assertNpmText = 'Dadadadash'
    const assertVersion = { version: '123456789', link: 'somelinkhere', filename: 'test.7z' }
    const assertError = new Error('Nagisa')
    const assertTarget = util.getPathFromRoot('./testapp/123456789')
    const assertTargetCheck = util.getPathFromRoot('./testapp/123456789/')
    const stubUpdated = stub()
    provider.getLatestVersion.resolves(assertVersion)
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0)

    stubExtract.returnWith(function(target, stream) {
      stream(assertExtractText)
      return Promise.resolve()
    })
    stubRunCommand.returnWith(function(command, options, folder, stream) {
      stream(assertNpmText)
      return Promise.reject(assertError)
    })

    app.once('updated', stubUpdated)

    let err = await assert.isRejected(app.update())

    assert.notOk(stubUpdated.called)
    assert.strictEqual(app.updating, false)
    assert.strictEqual(err, assertError)
    assert.strictEqual(stubRunCommand.firstCall[0], util.getNpmExecutable())
    assert.ok(stubRunCommand.firstCall[1])
    assert.strictEqual(stubRunCommand.firstCall[1][0], 'install')
    assert.ok(stubRunCommand.firstCall[1].includes('--production'), 'should have --production')
    assert.ok(stubRunCommand.firstCall[1].includes('--no-optional'), 'should have --no-optional')
    assert.ok(stubRunCommand.firstCall[1].includes('--no-package-lock'), 'should have --no-package-lock')
    assert.ok(stubRunCommand.firstCall[1].includes('--no-audit'), 'should have --no-audit')
    assert.strictEqual(stubRunCommand.firstCall[2], assertTarget)

    for (let i = 0; i < stubFsRm.callCount; i++) {
      assert.notStrictEqual(stubFsRm.getCall(i)[0], assertTarget)
      assert.notStrictEqual(stubFsRm.getCall(i)[0], assertTargetCheck)
    }
    
    assert.ok(stubWrite.called)
    assert.ok(stubWrite.callCount >= 4)
    assert.match(ctx.db.data.core.testapp.updater, /npm/i)
    assert.match(ctx.db.data.core.testapp.updater, /install/i)
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1)
    assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion)
    assert.ok(assertVersion.log)
    assert.match(assertVersion.log, /\. \n/)
    assert.match(assertVersion.log, new RegExp(assertExtractText))
    assert.match(assertVersion.log, new RegExp(assertNpmText))
    assert.match(assertVersion.log, new RegExp(assertError.message))
    assert.match(assertVersion.log, /npm/i)
    assert.match(assertVersion.log, /install/i)
    assert.ok(assertVersion.log.endsWith('\n'))
    assert.strictEqual(assertVersion.failtoinstall, 1)
    
    // Test if subsequent calls increment counter

    err = await assert.isRejected(app.update())
    
    assert.strictEqual(app.updating, false)
    assert.strictEqual(err, assertError)
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1)
    assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion)
    assert.strictEqual(assertVersion.failtoinstall, 2)
  })
  
  t.test('should update latest installed correctly', async function() {
    const assertVersion = { version: '123456789', link: 'httplinkhere', filename: 'test.7z' }
    provider.getLatestVersion.resolves(assertVersion)
    assert.notStrictEqual(ctx.db.data.core.testapp.latestInstalled, assertVersion.version)
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0)

    let result = await new Promise(function(res, rej) {
      app.once('updated', res)
      app.update().catch(rej)
    })

    assert.strictEqual(app.updating, false)
    assert.strictEqual(ctx.db.data.core.testapp.latestInstalled, assertVersion.version)
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1)
    assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion)
    assert.strictEqual(result, assertVersion)
    assert.ok(assertVersion.log)
    assert.ok(stubWrite.callCount >= 4)
    assert.strictEqual(assertVersion.installed, true)
    assert.strictEqual(assertVersion.stable, 0)
    assert.match(assertVersion.log, /found/i)
    assert.match(assertVersion.log, new RegExp(assertVersion.version))
    assert.match(assertVersion.log, /downloading/i)
    assert.match(assertVersion.log, new RegExp('extracting[^.]+file\.7z', 'i'))
    assert.match(assertVersion.log, /finished/i)
    assert.match(assertVersion.log, /updating/i)
  })

  t.test('should update existing version if found', async function() {
    const assertNewLink = 'THE last pain'
    const assertNewFilename = 'The place of hope.7z'
    const assertStable = 100
    const stubUpdated = stub()
    const oldLog = 'The Smell of Sea\n'
    const assertVersion = { version: '123456789', link: 'httplinkhere', filename: 'test.7z', log: oldLog, stable: assertStable }
    
    assertVersion.id = assertVersion.version
    ctx.db.upsert(ctx.db.data.core.testapp.versions, assertVersion)

    provider.getLatestVersion.resolves({ version: assertVersion.version, link: assertNewLink, filename: assertNewFilename })
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1)

    app.once('updated', stubUpdated)

    let result = await app.update()

    assert.ok(stubUpdated.called)
    assert.strictEqual(result, stubUpdated.firstCall[0])
    
    assert.ok(assertVersion.log)
    assert.ok(stubWrite.callCount >= 4)
    assert.strictEqual(app.updating, false)
    assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1)
    assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion)
    assert.strictEqual(result, assertVersion)
    assert.strictEqual(assertVersion.stable, assertStable)
    assert.ok(assertVersion.log)
    assert.ok(assertVersion.log.startsWith(oldLog))
    assert.strictEqual(assertVersion.installed, true)
    assert.match(assertVersion.log, /found/i)
    assert.match(assertVersion.log, new RegExp(assertVersion.version))
    assert.match(assertVersion.log, /downloading/i)
    assert.match(assertVersion.log, new RegExp('extracting[^.]+file\.7z', 'i'))
    assert.match(assertVersion.log, /finished/i)
    assert.match(assertVersion.log, /updating/i)
  })
  
  t.test('should do nothing if latestInstalled matches version', async function() {
    const assertError = new Error('should not be seen')
    const assertVersion = { version: '999.888.777.666', filename: 'test.7z' }
    const stubUpdated = stub()
    provider.getLatestVersion.resolves(assertVersion)
    provider.downloadVersion.rejects(assertError)
    ctx.db.data.core.testapp.updater = ''
    ctx.db.data.core.testapp.latestInstalled = assertVersion.version

    app.once('updated', stubUpdated)

    let result = await app.update()

    assert.strictEqual(result, null)
    assert.notOk(stubUpdated.called)

    result = await app.update()
    
    assert.notOk(stubUpdated.called)
    assert.strictEqual(result, null)
    assert.ok(stubWrite.callCount >= 1)
    assert.strictEqual(app.updating, false)
    assert.notOk(provider.downloadVersion.called)
    assert.match(ctx.db.data.core.testapp.updater, /already/i)
    assert.match(ctx.db.data.core.testapp.updater, /nothing/i)
  })

  t.test('should do nothing if installed version is found', async function() {
    const assertError = new Error('should not be seen')
    const assertVersion = { version: '999.888.777.666', filename: 'test.7z' }
    const stubUpdated = stub()
    provider.getLatestVersion.resolves(assertVersion)
    provider.downloadVersion.rejects(assertError)
    ctx.db.data.core.testapp.updater = ''

    ctx.db.upsert(ctx.db.data.core.testapp.versions, { id: '111.111.111.111', installed: true })
    ctx.db.upsert(ctx.db.data.core.testapp.versions, { id: '222.222.222.222', installed: true })
    ctx.db.upsert(ctx.db.data.core.testapp.versions, { id: '999.888.777.666', installed: true })
    ctx.db.upsert(ctx.db.data.core.testapp.versions, { id: '333.333.333.333', installed: true })

    app.once('updated', stubUpdated)

    await app.update()
    
    assert.notOk(stubUpdated.called)
    assert.ok(stubWrite.callCount >= 1)
    assert.strictEqual(app.updating, false)
    assert.notOk(provider.downloadVersion.called)
    assert.match(ctx.db.data.core.testapp.updater, /already/i)
    assert.match(ctx.db.data.core.testapp.updater, /nothing/i)
  })

  t.test('should do nothing it exists and failtodownload is higher than 3', async function() {
    const assertError = new Error('should not be seen')
    const assertVersion = { version: '999.888.777.666', filename: 'test.7z' }
    const stubUpdated = stub()
    provider.getLatestVersion.resolves(assertVersion)
    provider.downloadVersion.rejects(assertError)
    ctx.db.data.core.testapp.updater = ''
    ctx.db.upsert(ctx.db.data.core.testapp.versions, { id: '999.888.777.666', version: '999.888.777.666', link: 'httplinkhere', filename: 'test.7z', failtodownload: 4 })

    app.once('updated', stubUpdated)

    let result = await app.update()
    
    assert.notOk(stubUpdated.called)
    assert.strictEqual(result, null)
    assert.ok(stubWrite.callCount >= 1)
    assert.strictEqual(app.updating, false)
    assert.notOk(provider.downloadVersion.called)
    assert.match(ctx.db.data.core.testapp.updater, /many/i)
    assert.match(ctx.db.data.core.testapp.updater, /fail/i)
    assert.match(ctx.db.data.core.testapp.updater, /skip/i)
  })

  t.test('should do nothing it exists and failtoinstall is higher than 3', async function() {
    const assertError = new Error('should not be seen')
    const assertVersion = { version: '999.888.777.666', filename: 'test.7z' }
    const stubUpdated = stub()
    provider.getLatestVersion.resolves(assertVersion)
    provider.downloadVersion.rejects(assertError)
    ctx.db.data.core.testapp.updater = ''
    ctx.db.upsert(ctx.db.data.core.testapp.versions, { id: '999.888.777.666', version: '999.888.777.666', link: 'httplinkhere', filename: 'test.7z', failtoinstall: 4 })

    app.once('updated', stubUpdated)

    let result = await app.update()
    
    assert.notOk(stubUpdated.called)
    assert.strictEqual(result, null)
    assert.ok(stubWrite.callCount >= 1)
    assert.strictEqual(app.updating, false)
    assert.notOk(provider.downloadVersion.called)
    assert.match(ctx.db.data.core.testapp.updater, /many/i)
    assert.match(ctx.db.data.core.testapp.updater, /fail/i)
    assert.match(ctx.db.data.core.testapp.updater, /skip/i)
  })
})

t.describe('#registerModule()', function() {
  const assertAppName = 'testappregister'
  let ctx
  let app
  
  t.beforeEach(function() {
    return createFakeContext()
      .then(function(res) {
        ctx = res
        let provider = new StaticProvider()
        app = new Application(ctx, provider, assertAppName)
      })
  })

  t.test('should fail if not an object with start', function() {
    let tests = [
      [1, 'number'],
      [0, 'empty number'],
      ['text', 'string'],
      ['', 'empty string'],
      [{}, 'empty object'],
      [[], 'array'],
      [{ a: 1 }, 'non-empty object'],
    ]

    tests.forEach(function(check) {
      assert.throws(function() {
        app.registerModule(check[0])
      }, function(err) {
        assert.match(err.message, /registerModule/i)
        assert.match(err.message, /function/i)
        assert.match(err.message, /start/i)
        assert.match(err.message, new RegExp(assertAppName, 'i'))
        return true
      }, `should fail if called with ${check[1]}`)
    })
  })

  t.test('should otherwise succeed if object has function start', function() {
    const assertFunction = function() {}
    const assertModule = { start: assertFunction }

    app.registerModule(assertModule)

    assert.strictEqual(app.module, assertModule)

    app.registerModule(assertFunction)
    
    assert.notStrictEqual(app.module, assertModule)
    assert.deepStrictEqual(app.module, assertModule)
  })
})
